claude-os/templates/skills/initialize-project/code_indexer.py
Bob Roberts ddf3bac39b CRITICAL FIXES: Consolidate templates + API improvements
This commit fixes several critical issues found during comprehensive review:

## CRITICAL FIX: Template Consolidation ⚠️
- Moved 6 commands from ~/.claude/commands/ to templates/commands/
- Moved 3 skills from ~/.claude/skills/ to templates/skills/
- Team sharing was BROKEN without this (symlinks pointed to empty dirs)
- Updated README with prominent consolidation step warning
- Commands: list, remember, save, search, session, triggers
- Skills: initialize-project, remember-this, memory

## HIGH PRIORITY FIXES:
- ✅ Completed selective semantic indexing (removed TODO at line 878)
- ✅ Fixed PostgreSQL references in health check (system uses SQLite)
- ✅ Created comprehensive API documentation (docs/API_REFERENCE.md)
- ✅ Added Depends import to fix authentication endpoints

## DOCUMENTATION:
- Added complete API_REFERENCE.md (60+ endpoints documented)
- Created HYBRID_INDEXING_INTEGRATION.md (5 KB architecture)
- Updated README with API docs link + consolidation warning
- Documented all hybrid indexing endpoints with examples

## IMPROVEMENTS:
- Selective semantic indexing now actually ingests files (was placeholder)
- Health check now correctly queries SQLite instead of PostgreSQL
- All authentication endpoints now functional
- Structural indexing stores as pure JSON (no embeddings)

Impact: Team sharing now works, all features documented, critical bugs fixed.
Tested: Pistn indexing (3,117 files in 3 seconds) ✅

🚀 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-31 14:23:43 -05:00

321 lines
12 KiB
Python

#!/usr/bin/env python3
"""
Smart Code Indexer for Claude OS
Intelligently chunks and indexes ALL code files in a project.
Supports multiple languages and formats.
"""
import os
import re
import time
from pathlib import Path
from typing import List, Dict, Tuple
import urllib.request
import urllib.error
import json
class CodeIndexer:
"""Intelligently index project code for semantic search."""
def __init__(self, project_path: str, project_id: int, api_url: str = "http://localhost:8051", max_files: int = 150):
self.project_path = Path(project_path).resolve()
self.project_id = project_id
self.api_url = api_url
self.max_files = max_files # Limit for faster indexing
# File patterns to index
self.include_patterns = {
'rb': ['app/**/*.rb', 'lib/**/*.rb', 'spec/**/*.rb', 'config/**/*.rb'],
'js': ['app/javascript/**/*.js', 'app/assets/javascripts/**/*.js', '**/*.js'],
'ts': ['**/*.ts', '**/*.tsx'],
'jsx': ['**/*.jsx'],
'json': ['package.json', 'Gemfile.lock', '*.json'],
'yaml': ['**/*.yml', '**/*.yaml'],
'md': ['README.md', '**/*.md'],
}
# Directories to skip
self.skip_dirs = {
'.git', 'node_modules', '.venv', 'venv', '__pycache__',
'dist', 'build', '.next', '.nuxt', 'coverage', 'tmp',
'.claude-os', '.env', '.DS_Store', '.bundle', 'vendor',
'test', 'tests', 'spec', 'specs', '__tests__', '.turbo'
}
self.indexed_count = 0
self.indexed_files = []
def _should_skip(self, path: Path) -> bool:
"""Check if path should be skipped."""
for skip_dir in self.skip_dirs:
if skip_dir in path.parts:
return True
return False
def _find_files(self) -> List[Path]:
"""Find all indexable files in project, prioritizing source files."""
source_files = []
config_files = []
doc_files = []
for root, dirs, filenames in os.walk(self.project_path):
# Remove skip directories
dirs[:] = [d for d in dirs if d not in self.skip_dirs]
for filename in filenames:
filepath = Path(root) / filename
if self._should_skip(filepath):
continue
# Check file extension and categorize
if filepath.suffix in ['.rb', '.js', '.ts', '.tsx', '.jsx']:
source_files.append(filepath)
elif filepath.suffix in ['.json', '.yml', '.yaml']:
config_files.append(filepath)
elif filepath.suffix in ['.md']:
doc_files.append(filepath)
# Combine in priority order: source files, config files, docs
files = sorted(source_files) + sorted(config_files) + sorted(doc_files)
# Limit to max_files for faster indexing
files = files[:self.max_files]
return files
def _parse_ruby(self, content: str, filepath: str) -> List[Dict]:
"""Parse Ruby file into logical chunks."""
chunks = []
lines = content.split('\n')
current_class = None
current_method = None
buffer = []
buffer_start = 0
for i, line in enumerate(lines):
stripped = line.strip()
# Class definition
if stripped.startswith('class '):
if buffer:
chunks.append({
'type': 'method' if current_method else 'block',
'name': current_method or current_class or 'code',
'class': current_class,
'file': str(Path(filepath).relative_to(self.project_path)),
'line': buffer_start,
'content': '\n'.join(buffer),
'language': 'ruby'
})
buffer = []
match = re.match(r'class\s+(\w+)', stripped)
if match:
current_class = match.group(1)
current_method = None
buffer_start = i
# Method definition
elif stripped.startswith('def '):
if buffer and current_method:
chunks.append({
'type': 'method',
'name': current_method,
'class': current_class,
'file': str(Path(filepath).relative_to(self.project_path)),
'line': buffer_start,
'content': '\n'.join(buffer),
'language': 'ruby'
})
buffer = []
match = re.match(r'def\s+(\w+)', stripped)
if match:
current_method = match.group(1)
buffer_start = i
buffer.append(line)
# Add final chunk
if buffer:
chunks.append({
'type': 'method' if current_method else 'class' if current_class else 'file',
'name': current_method or current_class or Path(filepath).name,
'class': current_class,
'file': str(Path(filepath).relative_to(self.project_path)),
'line': buffer_start,
'content': '\n'.join(buffer),
'language': 'ruby'
})
return chunks
def _parse_javascript(self, content: str, filepath: str) -> List[Dict]:
"""Parse JavaScript/TypeScript file into logical chunks."""
chunks = []
lines = content.split('\n')
current_class = None
current_function = None
buffer = []
buffer_start = 0
for i, line in enumerate(lines):
stripped = line.strip()
# Class definition
if stripped.startswith('class '):
if buffer:
chunks.append({
'type': 'function' if current_function else 'block',
'name': current_function or current_class or 'code',
'class': current_class,
'file': str(Path(filepath).relative_to(self.project_path)),
'line': buffer_start,
'content': '\n'.join(buffer),
'language': 'javascript'
})
buffer = []
match = re.match(r'class\s+(\w+)', stripped)
if match:
current_class = match.group(1)
current_function = None
buffer_start = i
# Function definition
elif any(stripped.startswith(prefix) for prefix in ['function ', 'const ', 'let ', 'var ', 'async function', 'export ']):
if buffer and current_function:
chunks.append({
'type': 'function',
'name': current_function,
'class': current_class,
'file': str(Path(filepath).relative_to(self.project_path)),
'line': buffer_start,
'content': '\n'.join(buffer),
'language': 'javascript'
})
buffer = []
# Extract function name
match = re.search(r'(?:function|const|let|var)\s+(\w+)', stripped)
if match:
current_function = match.group(1)
buffer_start = i
buffer.append(line)
# Add final chunk
if buffer:
chunks.append({
'type': 'function' if current_function else 'class' if current_class else 'file',
'name': current_function or current_class or Path(filepath).name,
'class': current_class,
'file': str(Path(filepath).relative_to(self.project_path)),
'line': buffer_start,
'content': '\n'.join(buffer),
'language': 'javascript'
})
return chunks
def _chunk_file(self, filepath: Path) -> List[Dict]:
"""Chunk a file into logical units based on language."""
try:
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
# Skip empty files
if not content.strip():
return []
# Parse based on file type
if filepath.suffix == '.rb':
return self._parse_ruby(content, str(filepath))
elif filepath.suffix in ['.js', '.jsx', '.ts', '.tsx']:
return self._parse_javascript(content, str(filepath))
else:
# For other files, treat as single chunk
return [{
'type': 'file',
'name': filepath.name,
'file': str(filepath.relative_to(self.project_path)),
'line': 0,
'content': content,
'language': filepath.suffix.lstrip('.')
}]
except Exception as e:
print(f"⚠️ Error chunking {filepath}: {e}")
return []
def _ingest_chunk(self, chunk: Dict, kb_name: str) -> bool:
"""Ingest a single chunk to project-index MCP."""
try:
payload = {
"filename": chunk['file'],
"content": chunk['content'],
"mcp_type": "project_index",
"metadata": {
"type": chunk['type'],
"name": chunk['name'],
"class": chunk.get('class'),
"line": chunk['line'],
"language": chunk['language']
}
}
req_url = f"{self.api_url}/api/projects/{self.project_id}/ingest-document"
payload_json = json.dumps(payload).encode('utf-8')
req = urllib.request.Request(
req_url,
data=payload_json,
headers={'Content-Type': 'application/json'},
method='POST'
)
with urllib.request.urlopen(req, timeout=10) as response:
if response.status in [200, 201]:
return True
return False
except Exception as e:
print(f"⚠️ Error ingesting chunk: {e}")
return False
def run(self) -> Dict:
"""Index the entire project."""
print(f"\n🔍 Indexing all code files in {self.project_path.name}...")
# Get all files
files = self._find_files()
print(f"📂 Found {len(files)} files to index")
kb_name = f"{self.project_path.name}-project_index"
# Index each file
total_chunks = 0
for filepath in files:
chunks = self._chunk_file(filepath)
for chunk in chunks:
if self._ingest_chunk(chunk, kb_name):
total_chunks += 1
# Progress indicator
if (len(self.indexed_files) + 1) % 10 == 0:
print(f" ✓ Indexed {len(self.indexed_files) + 1}/{len(files)} files...")
self.indexed_files.append(str(filepath.relative_to(self.project_path)))
self.indexed_count = total_chunks
print(f"✅ Indexing complete!")
print(f" 📦 {total_chunks} chunks indexed from {len(files)} files")
print(f" 📍 Indexed to: {kb_name}")
return {
"total_files": len(files),
"total_chunks": total_chunks,
"files_indexed": self.indexed_files
}