Skip to main content
Glama
auto-capture-hook.ps111.7 kB
<# .SYNOPSIS Claude Code Auto-Capture Hook (PowerShell Version) .DESCRIPTION Automatically captures valuable conversation content after tool operations. Uses pattern detection to identify decisions, errors, learnings, and implementations. Trigger: PostToolUse (Edit, Write, Bash) Input: JSON via stdin with transcript_path and tool info .NOTES File Name : auto-capture-hook.ps1 Version : 1.0.0 Platform : Windows #> param( [switch]$Debug ) $ErrorActionPreference = "SilentlyContinue" #region Configuration $DEFAULT_ENDPOINT = "http://127.0.0.1:8000" $DEFAULT_MIN_LENGTH = 300 $DEFAULT_MAX_LENGTH = 4000 $TIMEOUT_SECONDS = 5 #endregion #region Pattern Definitions $Patterns = @{ "Decision" = @{ Regex = "(decided|chose|will use|let's go with|i'll use|we'll use|settled on|going with|picked|selected|opting for|entschieden|gewählt|nehmen wir|verwenden wir|machen wir|nutzen wir|ausgewählt)" MemoryType = "Decision" Priority = 1 Confidence = 0.9 } "Error" = @{ Regex = "(error|exception|failed|fixed|bug|issue|crash|broken|resolved|solved|debugging|debugged|patched|fehler|behoben|gefixt|problem|kaputt|gelöst|repariert|fehlerbehebung)" MemoryType = "Error" Priority = 2 Confidence = 0.85 } "Learning" = @{ Regex = "(learned|discovered|realized|found out|turns out|interestingly|til|understanding now|now i see|aha|insight|gelernt|entdeckt|herausgefunden|stellte sich heraus|interessanterweise|jetzt verstehe ich)" MemoryType = "Learning" Priority = 3 Confidence = 0.85 } "Implementation" = @{ Regex = "(implemented|created|built|added|refactored|set up|configured|deployed|developed|wrote|coding|programmed|implementiert|erstellt|gebaut|hinzugefügt|konfiguriert|eingerichtet|refaktoriert|entwickelt|programmiert)" MemoryType = "Learning" Priority = 4 Confidence = 0.8 } "Important" = @{ Regex = "(critical|important|remember|note|key|essential|must|never|always|crucial|vital|significant|wichtig|merken|notiz|niemals|immer|kritisch|wesentlich|unbedingt|entscheidend)" MemoryType = "Context" Priority = 5 Confidence = 0.75 } "Code" = @{ Regex = "(function|class|component|api|endpoint|database|schema|test|config|module|interface|method|funktion|klasse|komponente|datenbank|schnittstelle|konfiguration|modul)" MemoryType = "Context" Priority = 6 Confidence = 0.7 MinLength = 600 } } $UserOverrides = @{ ForceRemember = "#remember" ForceSkip = "#skip" } #endregion #region Functions function Write-DebugLog { param([string]$Message) if ($Debug) { Write-Host "[auto-capture] $Message" -ForegroundColor Cyan } } function Get-HookConfig { $configPath = Join-Path $PSScriptRoot "..\config.json" if (Test-Path $configPath) { try { $config = Get-Content $configPath -Raw | ConvertFrom-Json return @{ Endpoint = if ($config.memoryService.http.endpoint) { $config.memoryService.http.endpoint } else { $DEFAULT_ENDPOINT } ApiKey = $config.memoryService.http.apiKey MinLength = if ($config.autoCapture.minLength) { $config.autoCapture.minLength } else { $DEFAULT_MIN_LENGTH } MaxLength = if ($config.autoCapture.maxLength) { $config.autoCapture.maxLength } else { $DEFAULT_MAX_LENGTH } Enabled = if ($null -ne $config.autoCapture.enabled) { $config.autoCapture.enabled } else { $true } DebugMode = if ($config.autoCapture.debugMode) { $config.autoCapture.debugMode } else { $false } } } catch { Write-DebugLog "Failed to load config: $_" } } return @{ Endpoint = $DEFAULT_ENDPOINT ApiKey = "" MinLength = $DEFAULT_MIN_LENGTH MaxLength = $DEFAULT_MAX_LENGTH Enabled = $true DebugMode = $false } } function Get-TranscriptContent { param([string]$TranscriptPath) if (-not (Test-Path $TranscriptPath)) { Write-DebugLog "Transcript not found: $TranscriptPath" return $null } try { $transcript = Get-Content $TranscriptPath -Raw | ConvertFrom-Json $lastUser = "" $lastAssistant = "" for ($i = $transcript.Count - 1; $i -ge 0; $i--) { $msg = $transcript[$i] $role = if ($msg.role) { $msg.role } else { $msg.type } if (-not $lastAssistant -and $role -eq "assistant") { $lastAssistant = Get-TextContent $msg.content } if (-not $lastUser -and $role -eq "user") { $lastUser = Get-TextContent $msg.content } if ($lastUser -and $lastAssistant) { break } } return @{ UserMessage = $lastUser AssistantMessage = $lastAssistant Combined = "User: $lastUser`n`nAssistant: $lastAssistant" } } catch { Write-DebugLog "Failed to parse transcript: $_" return $null } } function Get-TextContent { param($Content) if ($Content -is [string]) { return $Content } if ($Content -is [array]) { $texts = $Content | Where-Object { $_.type -eq "text" } | ForEach-Object { $_.text } return $texts -join "`n" } return "" } function Test-UserOverride { param([string]$UserMessage) return @{ ForceRemember = $UserMessage -match $UserOverrides.ForceRemember ForceSkip = $UserMessage -match $UserOverrides.ForceSkip } } function Find-Pattern { param( [string]$Content, [int]$MinLength ) if ($Content.Length -lt $MinLength) { return @{ IsValuable = $false Reason = "Content too short ($($Content.Length) < $MinLength chars)" } } $contentLower = $Content.ToLower() # Sort patterns by priority $sortedPatterns = $Patterns.GetEnumerator() | Sort-Object { $_.Value.Priority } foreach ($pattern in $sortedPatterns) { $patternDef = $pattern.Value # Check minimum length for this pattern if ($patternDef.MinLength -and $Content.Length -lt $patternDef.MinLength) { continue } if ($contentLower -match $patternDef.Regex) { Write-DebugLog "Matched pattern: $($pattern.Key)" return @{ IsValuable = $true MemoryType = $patternDef.MemoryType MatchedPattern = $pattern.Key Confidence = $patternDef.Confidence } } } return @{ IsValuable = $false Reason = "No pattern matched" } } function Get-ProjectName { param([string]$Cwd) if (-not $Cwd) { return $null } $parts = $Cwd -split "[/\\]" | Where-Object { $_ } $lastPart = $parts[-1] $skipDirs = @("home", "users", "documents", "desktop", "repositories", "projects", "src") if ($skipDirs -contains $lastPart.ToLower()) { return if ($parts.Count -gt 1) { $parts[-2] } else { $null } } return $lastPart } function Get-AutoTags { param( [hashtable]$Detection, [string]$ProjectName ) $tags = @("auto-captured", "smart-ingest") if ($Detection.MemoryType) { $tags += $Detection.MemoryType.ToLower() } if ($Detection.MatchedPattern) { $tags += $Detection.MatchedPattern.ToLower() } if ($ProjectName) { $tags += $ProjectName } return $tags } function Limit-Content { param( [string]$Content, [int]$MaxLength ) if ($Content.Length -le $MaxLength) { return $Content } $truncated = $Content.Substring(0, $MaxLength) $lastSentence = $truncated.LastIndexOf(". ") if ($lastSentence -gt ($MaxLength * 0.8)) { return $truncated.Substring(0, $lastSentence + 1) + "`n[truncated]" } return $truncated + "`n[truncated]" } function Send-Memory { param( [hashtable]$Config, [string]$Content, [string]$MemoryType, [string[]]$Tags ) $uri = "$($Config.Endpoint)/api/memories" $body = @{ content = $Content memory_type = $MemoryType tags = $Tags metadata = @{ source = "auto-capture" hook = "PostToolUse" captured_at = (Get-Date).ToString("o") } } | ConvertTo-Json -Depth 3 $headers = @{ "Content-Type" = "application/json" } if ($Config.ApiKey) { $headers["X-API-Key"] = $Config.ApiKey } try { $response = Invoke-RestMethod -Uri $uri -Method Post -Body $body -Headers $headers -TimeoutSec $TIMEOUT_SECONDS return $response } catch { Write-DebugLog "Failed to store memory: $_" return $null } } #endregion #region Main Execution $startTime = Get-Date try { # Load configuration $config = Get-HookConfig $Debug = $Debug -or $config.DebugMode if (-not $config.Enabled) { Write-DebugLog "Auto-capture disabled in configuration" exit 0 } # Read stdin $stdinData = "" if ([Console]::In.Peek() -ne -1) { $stdinData = [Console]::In.ReadToEnd() } if (-not $stdinData) { Write-DebugLog "No stdin input" exit 0 } try { $input = $stdinData | ConvertFrom-Json } catch { Write-DebugLog "Invalid JSON input" exit 0 } $transcriptPath = if ($input.transcript_path) { $input.transcript_path } else { $input.transcriptPath } $cwd = if ($input.cwd) { $input.cwd } else { (Get-Location).Path } if (-not $transcriptPath) { Write-DebugLog "No transcript path provided" exit 0 } # Parse transcript $transcript = Get-TranscriptContent -TranscriptPath $transcriptPath if (-not $transcript) { exit 0 } # Check user overrides $overrides = Test-UserOverride -UserMessage $transcript.UserMessage if ($overrides.ForceSkip) { Write-DebugLog "Skipped by user override (#skip)" exit 0 } $content = $transcript.Combined # Detect patterns if ($overrides.ForceRemember) { $detection = @{ IsValuable = $true MemoryType = "Context" MatchedPattern = "user-override" Confidence = 1.0 } Write-DebugLog "Force remember by user override (#remember)" } else { $detection = Find-Pattern -Content $content -MinLength $config.MinLength } if (-not $detection.IsValuable) { Write-DebugLog "Not valuable: $($detection.Reason)" exit 0 } # Prepare and store $truncatedContent = Limit-Content -Content $content -MaxLength $config.MaxLength $projectName = Get-ProjectName -Cwd $cwd $tags = Get-AutoTags -Detection $detection -ProjectName $projectName Write-DebugLog "Storing $($detection.MemoryType) memory..." Write-DebugLog "Pattern: $($detection.MatchedPattern)" Write-DebugLog "Tags: $($tags -join ', ')" $result = Send-Memory -Config $config -Content $truncatedContent -MemoryType $detection.MemoryType -Tags $tags $elapsed = ((Get-Date) - $startTime).TotalMilliseconds if ($result) { Write-DebugLog "Stored successfully in ${elapsed}ms" } exit 0 } catch { $elapsed = ((Get-Date) - $startTime).TotalMilliseconds Write-Host "[auto-capture] Error after ${elapsed}ms: $_" -ForegroundColor Red exit 0 } #endregion

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/doobidoo/mcp-memory-service'

If you have feedback or need assistance with the MCP directory API, please join our Discord server