Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 228 additions & 0 deletions .claude/hooks/index-solution.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
#!/usr/bin/env pwsh
# Solution Indexer Hook for Claude Code
# Triggers after file modifications to rebuild solution index

param()

$ErrorActionPreference = "Stop"

# Configuration
$SOLUTION_ROOT = if ($env:CLAUDE_PROJECT_DIR) { $env:CLAUDE_PROJECT_DIR } else { Get-Location }
$INDEX_FILE = Join-Path $SOLUTION_ROOT ".claude\solution-index.json"
$LOG_FILE = Join-Path $SOLUTION_ROOT ".claude\hooks\indexer.log"

# Ensure directories exist
$indexDir = Split-Path $INDEX_FILE -Parent
if (-not (Test-Path $indexDir)) {
New-Item -ItemType Directory -Path $indexDir -Force | Out-Null
}

# Function to write log
function Write-IndexLog {
param([string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
"$timestamp - $Message" | Add-Content -Path $LOG_FILE -ErrorAction SilentlyContinue
}

try {
# Read hook input from stdin
$inputJson = [Console]::In.ReadToEnd()
$hookData = $inputJson | ConvertFrom-Json

# Extract relevant information
$toolName = $hookData.tool_name
$toolInput = $hookData.tool_input
$eventName = $hookData.hook_event_name

Write-IndexLog "Hook triggered: $eventName for tool $toolName"

# Determine which file was modified
$modifiedFile = $null
switch ($toolName) {
"Write" { $modifiedFile = $toolInput.file_path }
"Edit" { $modifiedFile = $toolInput.file_path }
"MultiEdit" { $modifiedFile = $toolInput.file_path }
}

if ($modifiedFile) {
Write-IndexLog "File modified: $modifiedFile"
}

# Start indexing in background (non-blocking)
$indexJob = Start-Job -ScriptBlock {
param($Root, $IndexFile, $LogFile)

function Write-JobLog {
param([string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
"$timestamp - [INDEXER] $Message" | Add-Content -Path $LogFile -ErrorAction SilentlyContinue
}

Write-JobLog "Starting solution index rebuild..."

# Build the index
$index = @{
timestamp = (Get-Date).ToUniversalTime().ToString("o")
root = $Root
statistics = @{}
files = @()
structure = @{}
}

# Define file patterns to index
$includePatterns = @(
"*.cs", "*.vb", "*.fs", # .NET languages
"*.csproj", "*.vbproj", "*.fsproj", "*.sln", # Project files
"*.js", "*.jsx", "*.ts", "*.tsx", # JavaScript/TypeScript
"*.py", # Python
"*.ps1", "*.psm1", "*.psd1", # PowerShell
"*.cpp", "*.hpp", "*.c", "*.h", # C/C++
"*.java", # Java
"*.go", # Go
"*.rs", # Rust
"*.php", # PHP
"*.rb", # Ruby
"*.swift", # Swift
"*.kt", "*.kts", # Kotlin
"*.json", "*.xml", "*.yaml", "*.yml", # Config files
"*.md", "*.txt", # Documentation
"*.html", "*.css", "*.scss", "*.sass" # Web files
)

# Define directories to exclude
$excludeDirs = @(
".git", ".svn", ".hg",
"node_modules", "packages", ".nuget",
"bin", "obj", "Debug", "Release",
"dist", "build", "out",
".vs", ".vscode", ".idea",
"__pycache__", ".pytest_cache",
"venv", "env", ".env"
)

# Build exclude regex
$excludeRegex = ($excludeDirs | ForEach-Object { [regex]::Escape($_) }) -join '|'
$excludeRegex = "[\\/]($excludeRegex)[\\/]"

# Collect all files
$allFiles = @()
foreach ($pattern in $includePatterns) {
$files = Get-ChildItem -Path $Root -Filter $pattern -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -notmatch $excludeRegex }
$allFiles += $files
}

Write-JobLog "Found $($allFiles.Count) files to index"

# Process files
$filesByExtension = @{}
$totalSize = 0

foreach ($file in $allFiles) {
$relativePath = $file.FullName.Substring($Root.Length).TrimStart('\', '/')
$ext = $file.Extension.ToLower()

# Track statistics
if (-not $filesByExtension.ContainsKey($ext)) {
$filesByExtension[$ext] = 0
}
$filesByExtension[$ext]++
$totalSize += $file.Length

# Add to index
$index.files += @{
path = $relativePath
name = $file.Name
extension = $ext
size = $file.Length
modified = $file.LastWriteTimeUtc.ToString("o")
directory = (Split-Path $relativePath -Parent) -replace '\\', '/'
}
}

# Build directory structure
$dirs = @{}
foreach ($file in $index.files) {
$parts = $file.directory -split '/'
$current = $dirs

foreach ($part in $parts) {
if ($part -and $part -ne ".") {
if (-not $current.ContainsKey($part)) {
$current[$part] = @{}
}
$current = $current[$part]
}
}
}
$index.structure = $dirs

# Update statistics
$index.statistics = @{
totalFiles = $allFiles.Count
totalSize = $totalSize
totalSizeMB = [math]::Round($totalSize / 1MB, 2)
filesByExtension = $filesByExtension
lastUpdated = (Get-Date).ToUniversalTime().ToString("o")
}

# Look for solution files
$slnFiles = Get-ChildItem -Path $Root -Filter "*.sln" -File -ErrorAction SilentlyContinue
if ($slnFiles) {
$index.solutions = $slnFiles | ForEach-Object {
@{
name = $_.Name
path = $_.FullName.Substring($Root.Length).TrimStart('\', '/')
}
}
Write-JobLog "Found $($slnFiles.Count) solution file(s)"
}

# Look for project files
$projExtensions = @("*.csproj", "*.vbproj", "*.fsproj", "*.vcxproj", "*.pyproj", "*.njsproj")
$projFiles = @()
foreach ($ext in $projExtensions) {
$projFiles += Get-ChildItem -Path $Root -Filter $ext -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -notmatch $excludeRegex }
}

if ($projFiles) {
$index.projects = $projFiles | ForEach-Object {
@{
name = $_.BaseName
file = $_.Name
path = $_.FullName.Substring($Root.Length).TrimStart('\', '/')
type = $_.Extension.Substring(1, $_.Extension.Length - 5) # Remove . and proj
}
}
Write-JobLog "Found $($projFiles.Count) project file(s)"
}

# Save index
$index | ConvertTo-Json -Depth 10 -Compress | Set-Content -Path $IndexFile -Encoding UTF8
Write-JobLog "Index saved to $IndexFile"
Write-JobLog "Indexing complete: $($index.statistics.totalFiles) files, $($index.statistics.totalSizeMB) MB"

return @{
success = $true
filesIndexed = $index.statistics.totalFiles
sizeMB = $index.statistics.totalSizeMB
}

} -ArgumentList $SOLUTION_ROOT, $INDEX_FILE, $LOG_FILE

# Don't wait for job to complete (non-blocking)
Write-IndexLog "Indexing job started with ID: $($indexJob.Id)"

# Optional: Clean up old completed jobs
Get-Job | Where-Object { $_.State -eq 'Completed' -and $_.Name -notlike 'IndexJob*' } | Remove-Job -Force -ErrorAction SilentlyContinue

# Success - hook completes immediately while indexing continues in background
Write-Host "Solution indexing started in background (Job ID: $($indexJob.Id))"
exit 0

} catch {
Write-IndexLog "ERROR: $_"
Write-Error "Hook failed: $_"
exit 1
}
147 changes: 147 additions & 0 deletions .claude/hooks/query-index.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#!/usr/bin/env pwsh
# Query Solution Index
# Helper script to query the solution index created by the indexer hook

param(
[Parameter(Position=0)]
[string]$Query = "",

[Parameter()]
[ValidateSet("files", "stats", "projects", "structure", "recent", "large")]
[string]$Mode = "files"
)

$ErrorActionPreference = "Stop"

# Configuration
$SOLUTION_ROOT = if ($env:CLAUDE_PROJECT_DIR) { $env:CLAUDE_PROJECT_DIR } else { Get-Location }
$INDEX_FILE = Join-Path $SOLUTION_ROOT ".claude\solution-index.json"

if (-not (Test-Path $INDEX_FILE)) {
Write-Host "No index found. The index will be created after file modifications." -ForegroundColor Yellow
Write-Host "Index location: $INDEX_FILE"
exit 1
}

# Load index
$index = Get-Content $INDEX_FILE -Raw | ConvertFrom-Json

Write-Host "Solution Index - $($index.root)" -ForegroundColor Cyan
Write-Host "Last updated: $($index.statistics.lastUpdated)" -ForegroundColor Gray
Write-Host ""

switch ($Mode) {
"stats" {
Write-Host "Statistics:" -ForegroundColor Green
Write-Host " Total files: $($index.statistics.totalFiles)"
Write-Host " Total size: $($index.statistics.totalSizeMB) MB"
Write-Host ""
Write-Host "Files by extension:" -ForegroundColor Green
$index.statistics.filesByExtension.PSObject.Properties |
Sort-Object -Property Value -Descending |
Select-Object -First 15 |
ForEach-Object {
Write-Host (" {0,-10} {1,6} files" -f $_.Name, $_.Value)
}

if ($index.solutions) {
Write-Host ""
Write-Host "Solutions:" -ForegroundColor Green
$index.solutions | ForEach-Object {
Write-Host " $($_.name)"
}
}
}

"projects" {
if ($index.projects) {
Write-Host "Projects:" -ForegroundColor Green
$index.projects |
Sort-Object -Property type, name |
ForEach-Object {
Write-Host (" [{0,-6}] {1}" -f $_.type.ToUpper(), $_.name)
Write-Host (" {0}" -f $_.path) -ForegroundColor Gray
}
} else {
Write-Host "No project files found in index" -ForegroundColor Yellow
}
}

"structure" {
function Show-Tree {
param($Node, $Indent = "")

foreach ($key in $Node.PSObject.Properties.Name | Sort-Object) {
Write-Host "$Indent├── $key" -ForegroundColor DarkCyan
if ($Node.$key -and $Node.$key.PSObject.Properties.Count -gt 0) {
Show-Tree -Node $Node.$key -Indent "$Indent│ "
}
}
}

Write-Host "Directory Structure:" -ForegroundColor Green
Show-Tree -Node $index.structure
}

"recent" {
Write-Host "Recently Modified Files (last 20):" -ForegroundColor Green
$index.files |
Sort-Object -Property modified -Descending |
Select-Object -First 20 |
ForEach-Object {
$modified = [DateTime]::Parse($_.modified).ToLocalTime().ToString("yyyy-MM-dd HH:mm")
Write-Host (" {0} - {1}" -f $modified, $_.path)
}
}

"large" {
Write-Host "Largest Files (top 20):" -ForegroundColor Green
$index.files |
Sort-Object -Property size -Descending |
Select-Object -First 20 |
ForEach-Object {
$sizeMB = [math]::Round($_.size / 1MB, 2)
Write-Host (" {0,8} MB - {1}" -f $sizeMB, $_.path)
}
}

"files" {
if ($Query) {
Write-Host "Searching for: '$Query'" -ForegroundColor Green
$matches = $index.files | Where-Object {
$_.path -like "*$Query*" -or
$_.name -like "*$Query*"
}

if ($matches) {
Write-Host "Found $($matches.Count) matches:" -ForegroundColor Green
$matches | Select-Object -First 50 | ForEach-Object {
Write-Host " $($_.path)"
}

if ($matches.Count -gt 50) {
Write-Host " ... and $($matches.Count - 50) more" -ForegroundColor Gray
}
} else {
Write-Host "No files matching '$Query'" -ForegroundColor Yellow
}
} else {
Write-Host "File Extensions in Project:" -ForegroundColor Green
$extensions = $index.files |
Group-Object -Property extension |
Sort-Object -Property Count -Descending |
Select-Object -First 20

$extensions | ForEach-Object {
Write-Host (" {0,-10} {1,6} files" -f $_.Name, $_.Count)
}

Write-Host ""
Write-Host "Use -Query parameter to search for specific files" -ForegroundColor Gray
Write-Host "Example: .\query-index.ps1 -Query 'controller' -Mode files" -ForegroundColor Gray
}
}
}

Write-Host ""
Write-Host "Other modes: -Mode [stats|projects|structure|recent|large|files]" -ForegroundColor Gray
Loading