mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-05 23:03:55 +00:00
7a061c85eb
* refactor(hooks): simplify hook execution with shared context - [x] Extract BaseInput struct to eliminate duplicate fields across hooks - [x] Create RunHook handler pattern for session-start and user-prompt - [x] Create RunStatuslineHook for fast statusline rendering without worker startup - [x] Add HookContext struct to pass port, project, CWD, SessionID to handlers - [x] Add db/interface.go with ObservationReader/Writer interfaces - [x] Add comprehensive conflict management tests in sqlite/conflict_test.go - [x] Add vector client tests for Count, ModelVersion, NeedsRebuild, GetStaleVectors - [x] Add FilterByThreshold helper tests for query result filtering - [x] Make handlers_test more robust for network-dependent update checks - [x] Update package versions in UI * Move to GORM + general cleanup * feat(mcp): add observation relations discovery and scoring integration - [x] Add find_related_observations MCP tool for discovering related observations by confidence - [x] Integrate scoring calculator and recalculator into MCP server initialization - [x] Add pattern, relation, and session stores to MCP server dependencies - [x] Register MCP server in Claude Code settings during plugin installation - [x] Update install scripts (bash, PowerShell) to configure MCP server settings - [x] Switch plugin manifest files to template-based versioning (plugin.json.tpl, marketplace.json.tpl) - [x] Update all MCP server tests to pass new dependency parameters
120 lines
3.5 KiB
Go
120 lines
3.5 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
|
|
}
|
|
|
|
sessionID := int64(result["sessionDbId"].(float64))
|
|
promptNumber := int(result["promptNumber"].(float64))
|
|
|
|
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
|
|
}
|