Files
claude-mnemonic/cmd/hooks/user-prompt/main.go
T
lukaszraczylo e07d4174de fix(hooks,db,mcp,worker): add type safety and error handling (#21)
- [x] Add type checking and error handling for JSON type assertions in user-prompt hook
- [x] Add error handling for session update query in CreateSDKSession
- [x] Update MCP tool description to reference sqlite-vec instead of ChromaDB
- [x] Fix MinConfidence sentinel value check from 0 to -1
- [x] Pass project parameter to vector search filter in handleSearchByPrompt
- [x] Return empty map instead of nil for successful responses without JSON body
2026-01-09 22:17:05 +00:00

130 lines
3.8 KiB
Go

// Package main provides the user-prompt hook entry point.
package main
import (
"fmt"
"net/url"
"os"
"github.com/lukaszraczylo/claude-mnemonic/pkg/hooks"
)
// Input is the hook input from Claude Code.
type Input struct {
hooks.BaseInput
Prompt string `json:"prompt"`
}
func main() {
hooks.RunHook("UserPromptSubmit", handleUserPrompt)
}
func handleUserPrompt(ctx *hooks.HookContext, input *Input) (string, error) {
// Search for relevant observations based on the prompt
searchURL := fmt.Sprintf("/api/context/search?project=%s&query=%s&cwd=%s",
url.QueryEscape(ctx.Project),
url.QueryEscape(input.Prompt),
url.QueryEscape(ctx.CWD))
var contextToInject string
var observationCount int
searchResult, _ := hooks.GET(ctx.Port, searchURL)
if observations, ok := searchResult["observations"].([]interface{}); ok && len(observations) > 0 {
// Results are already filtered by relevance threshold and capped by max_results
// from the server-side config (ContextRelevanceThreshold, ContextMaxPromptResults)
observationCount = len(observations)
// Build context from search results
var contextBuilder string
contextBuilder = "<relevant-memory>\n"
contextBuilder += "# Relevant Knowledge From Previous Sessions\n"
contextBuilder += "IMPORTANT: Use this information to answer the question directly. Do NOT explore the codebase if the answer is here.\n\n"
for i, obs := range observations {
if obsMap, ok := obs.(map[string]interface{}); ok {
title := ""
if t, ok := obsMap["title"].(string); ok {
title = t
}
obsType := ""
if t, ok := obsMap["type"].(string); ok {
obsType = t
}
// Start observation block
contextBuilder += fmt.Sprintf("## %d. [%s] %s\n", i+1, obsType, title)
// Add facts first (most concise answers)
if facts, ok := obsMap["facts"].([]interface{}); ok && len(facts) > 0 {
contextBuilder += "Key facts:\n"
for _, fact := range facts {
if factStr, ok := fact.(string); ok {
contextBuilder += fmt.Sprintf("- %s\n", factStr)
}
}
contextBuilder += "\n"
}
// Add narrative if present
if narrative, ok := obsMap["narrative"].(string); ok && narrative != "" {
contextBuilder += narrative + "\n\n"
}
}
}
contextBuilder += "</relevant-memory>\n"
contextToInject = contextBuilder
}
// Initialize session with matched observations count
result, err := hooks.POST(ctx.Port, "/api/sessions/init", map[string]interface{}{
"claudeSessionId": ctx.SessionID,
"project": ctx.Project,
"prompt": input.Prompt,
"matchedObservations": observationCount,
})
if err != nil {
return "", err
}
// Check if skipped due to privacy
if skipped, ok := result["skipped"].(bool); ok && skipped {
fmt.Fprintf(os.Stderr, "[user-prompt] Session skipped (private)\n")
return "", nil
}
// Safely extract session ID and prompt number with type checking
sessionDbIdRaw, ok := result["sessionDbId"].(float64)
if !ok {
return "", fmt.Errorf("invalid or missing sessionDbId in response")
}
sessionID := int64(sessionDbIdRaw)
promptNumberRaw, ok := result["promptNumber"].(float64)
if !ok {
return "", fmt.Errorf("invalid or missing promptNumber in response")
}
promptNumber := int(promptNumberRaw)
fmt.Fprintf(os.Stderr, "[user-prompt] Session %d, prompt #%d\n", sessionID, promptNumber)
// Start SDK agent
_, err = hooks.POST(ctx.Port, fmt.Sprintf("/sessions/%d/init", sessionID), map[string]interface{}{
"userPrompt": input.Prompt,
"promptNumber": promptNumber,
})
if err != nil {
return "", err
}
// Return context if we found relevant observations
if observationCount > 0 {
// Show match count to user via stderr
fmt.Fprintf(os.Stderr, "[claude-mnemonic] Found %d relevant memories for this prompt\n", observationCount)
return contextToInject, nil
}
return "", nil
}