Files
claude-mnemonic/cmd/hooks/session-start/main.go
T
lukaszraczylo 74ae8ed4c1 feat(leann-phase2): implement hybrid vector storage and graph-based search
- [x] Add AST-aware code chunking for Go, Python, and TypeScript using tree-sitter
- [x] Implement LEANN-inspired hybrid vector storage with hub detection and selective embedding storage (60-80% savings)
- [x] Add observation relationship graph with CSR format and edge detection (file overlap, semantic similarity, temporal, concept)
- [x] Implement graph-aware search with two-level traversal and relationship-based ranking
- [x] Add auto-tuning system for dynamic hub threshold adjustment based on query performance
- [x] Add comprehensive metrics tracking for vector storage, queries, latency, and graph traversals
- [x] Update configuration system with graph and hybrid storage settings
- [x] Add graph stats and vector metrics endpoints to worker service
- [x] Enhance UI sidebar with advanced metrics display and graph visualization
- [x] Optimize struct field alignment throughout codebase for memory efficiency
- [x] Update documentation with LEANN Phase 2 features and performance benefits
- [x] Add tree-sitter dependency for AST parsing
2026-01-07 20:43:10 +00:00

130 lines
3.4 KiB
Go

// Package main provides the session-start hook entry point.
package main
import (
"fmt"
"net/url"
"os"
"strings"
"github.com/lukaszraczylo/claude-mnemonic/pkg/hooks"
)
// Input is the hook input from Claude Code.
type Input struct {
hooks.BaseInput
Source string `json:"source"` // "startup", "resume", "clear", "compact"
}
// Observation represents an observation from the API.
type Observation struct {
Type string `json:"type"`
Title string `json:"title"`
Subtitle string `json:"subtitle"`
Narrative string `json:"narrative"`
Facts []string `json:"facts"`
ID int64 `json:"id"`
}
func main() {
hooks.RunHook("SessionStart", handleSessionStart)
}
func handleSessionStart(ctx *hooks.HookContext, input *Input) (string, error) {
// Fetch observations for context injection
endpoint := fmt.Sprintf("/api/context/inject?project=%s&cwd=%s",
url.QueryEscape(ctx.Project),
url.QueryEscape(ctx.CWD))
result, err := hooks.GET(ctx.Port, endpoint)
if err != nil {
fmt.Fprintf(os.Stderr, "[claude-mnemonic] Warning: context fetch failed: %v\n", err)
return "", nil
}
// Parse observations from response
obsData, ok := result["observations"].([]interface{})
if !ok || len(obsData) == 0 {
// No observations - just continue normally
return "", nil
}
// Get full_count from response (how many observations get full detail)
fullCount := 25 // default
if fc, ok := result["full_count"].(float64); ok && fc > 0 {
fullCount = int(fc)
}
// Show count to user via stderr
fmt.Fprintf(os.Stderr, "[claude-mnemonic] Injecting %d observations from project memory (%d detailed, %d condensed)\n",
len(obsData), min(fullCount, len(obsData)), max(0, len(obsData)-fullCount))
// Build context string
contextBuilder := "<claude-mnemonic-context>\n"
contextBuilder += fmt.Sprintf("# Project Memory (%d observations)\n", len(obsData))
contextBuilder += "Use this knowledge to answer questions without re-exploring the codebase.\n\n"
for i, o := range obsData {
obs, ok := o.(map[string]interface{})
if !ok {
continue
}
title := getString(obs, "title")
obsType := getString(obs, "type")
// First `fullCount` observations get full detail, rest are condensed
if i < fullCount {
// Full detail: include narrative and facts
narrative := getString(obs, "narrative")
contextBuilder += fmt.Sprintf("## %d. [%s] %s\n", i+1, strings.ToUpper(obsType), title)
if narrative != "" {
contextBuilder += narrative + "\n"
}
if facts, ok := obs["facts"].([]interface{}); ok && len(facts) > 0 {
contextBuilder += "Key facts:\n"
for _, f := range facts {
if fact, ok := f.(string); ok && fact != "" {
contextBuilder += fmt.Sprintf("- %s\n", fact)
}
}
}
contextBuilder += "\n"
} else {
// Condensed: just title and subtitle (one line)
subtitle := getString(obs, "subtitle")
if subtitle != "" {
contextBuilder += fmt.Sprintf("- [%s] %s: %s\n", strings.ToUpper(obsType), title, subtitle)
} else {
contextBuilder += fmt.Sprintf("- [%s] %s\n", strings.ToUpper(obsType), title)
}
}
}
contextBuilder += "</claude-mnemonic-context>\n"
return contextBuilder, nil
}
func getString(m map[string]interface{}, key string) string {
if v, ok := m[key].(string); ok {
return v
}
return ""
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}