package handler import ( "crypto/sha256" "encoding/json" "fmt" "net/http" "strings" "time" "github.com/seifghazi/claude-code-monitor/internal/model" ) // SanitizeHeaders removes sensitive headers before logging/storage func SanitizeHeaders(headers http.Header) http.Header { sanitized := make(http.Header) sensitiveHeaders := []string{ "x-api-key", "api-key", "authorization", "anthropic-api-key", "openai-api-key", "bearer", } for key, values := range headers { lowerKey := strings.ToLower(key) isSensitive := false for _, sensitive := range sensitiveHeaders { if strings.Contains(lowerKey, sensitive) { isSensitive = true break } } if isSensitive { sanitized[key] = []string{"[REDACTED]"} } else { sanitized[key] = values } } return sanitized } // ConversationDiffAnalyzer analyzes conversation flows to identify new vs repeated content type ConversationDiffAnalyzer struct{} // NewConversationDiffAnalyzer creates a new conversation diff analyzer func NewConversationDiffAnalyzer() *ConversationDiffAnalyzer { return &ConversationDiffAnalyzer{} } // ConversationFlowData represents the flow analysis of a conversation type ConversationFlowData struct { TotalMessages int `json:"totalMessages"` NewMessages []int `json:"newMessages"` // Indices of new messages DuplicateMessages []int `json:"duplicateMessages"` // Indices of duplicate messages MessageHashes []string `json:"messageHashes"` // Content hashes for deduplication ConversationHash string `json:"conversationHash"` // Hash of entire conversation PreviousHash string `json:"previousHash"` // Hash of previous conversation state Changes []ConversationChange `json:"changes"` // Detailed changes FlowMetadata map[string]interface{} `json:"flowMetadata"` // Additional metadata } // ConversationChange represents a specific change in the conversation type ConversationChange struct { Type string `json:"type"` // "added", "modified", "context" MessageIdx int `json:"messageIdx"` // Index of the message Role string `json:"role"` // Role of the message ContentHash string `json:"contentHash"` // Hash of the content Preview string `json:"preview"` // Short preview of content Timestamp string `json:"timestamp"` // When this change was detected } // AnalyzeConversationFlow analyzes a conversation to identify what's new vs repeated func (c *ConversationDiffAnalyzer) AnalyzeConversationFlow(messages []model.AnthropicMessage, previousConversation []model.AnthropicMessage) *ConversationFlowData { totalMessages := len(messages) // Create hashes for current conversation currentHashes := make([]string, totalMessages) for i, msg := range messages { currentHashes[i] = c.hashMessage(msg) } // Create hashes for previous conversation (if any) var previousHashes []string if previousConversation != nil { previousHashes = make([]string, len(previousConversation)) for i, msg := range previousConversation { previousHashes[i] = c.hashMessage(msg) } } // Identify new vs duplicate messages newMessages := []int{} duplicateMessages := []int{} changes := []ConversationChange{} // Simple approach: messages that appear after the previous conversation length are new previousLength := len(previousHashes) for i, msg := range messages { isNew := i >= previousLength // More sophisticated check: compare hashes if !isNew && i < len(previousHashes) { isNew = currentHashes[i] != previousHashes[i] } if isNew { newMessages = append(newMessages, i) changes = append(changes, ConversationChange{ Type: "added", MessageIdx: i, Role: msg.Role, ContentHash: currentHashes[i], Preview: c.getMessagePreview(msg), Timestamp: fmt.Sprintf("%d", time.Now().Unix()), }) } else { duplicateMessages = append(duplicateMessages, i) changes = append(changes, ConversationChange{ Type: "context", MessageIdx: i, Role: msg.Role, ContentHash: currentHashes[i], Preview: c.getMessagePreview(msg), Timestamp: fmt.Sprintf("%d", time.Now().Unix()), }) } } // If no previous conversation, consider a reasonable threshold of "new" vs "context" if previousConversation == nil && totalMessages > 1 { // Heuristic: last 30% of messages are "new", rest is context newThreshold := max(1, int(float64(totalMessages)*0.3)) contextEnd := totalMessages - newThreshold newMessages = []int{} duplicateMessages = []int{} changes = []ConversationChange{} for i := 0; i < totalMessages; i++ { if i >= contextEnd { newMessages = append(newMessages, i) changes = append(changes, ConversationChange{ Type: "added", MessageIdx: i, Role: messages[i].Role, ContentHash: currentHashes[i], Preview: c.getMessagePreview(messages[i]), Timestamp: fmt.Sprintf("%d", time.Now().Unix()), }) } else { duplicateMessages = append(duplicateMessages, i) changes = append(changes, ConversationChange{ Type: "context", MessageIdx: i, Role: messages[i].Role, ContentHash: currentHashes[i], Preview: c.getMessagePreview(messages[i]), Timestamp: fmt.Sprintf("%d", time.Now().Unix()), }) } } } // Generate conversation hashes conversationHash := c.hashConversation(messages) previousHash := "" if previousConversation != nil { previousHash = c.hashConversation(previousConversation) } return &ConversationFlowData{ TotalMessages: totalMessages, NewMessages: newMessages, DuplicateMessages: duplicateMessages, MessageHashes: currentHashes, ConversationHash: conversationHash, PreviousHash: previousHash, Changes: changes, FlowMetadata: map[string]interface{}{ "newCount": len(newMessages), "duplicateCount": len(duplicateMessages), "analyzeTime": time.Now().Format(time.RFC3339), }, } } // hashMessage creates a hash of a message for deduplication func (c *ConversationDiffAnalyzer) hashMessage(msg model.AnthropicMessage) string { // Create a stable representation of the message content := c.normalizeMessageContent(msg.Content) data := fmt.Sprintf("%s|%s", msg.Role, content) hash := sha256.Sum256([]byte(data)) return fmt.Sprintf("%x", hash[:8]) // Use first 8 bytes for shorter hash } // hashConversation creates a hash of the entire conversation func (c *ConversationDiffAnalyzer) hashConversation(messages []model.AnthropicMessage) string { var parts []string for _, msg := range messages { parts = append(parts, c.hashMessage(msg)) } conversationData := strings.Join(parts, "|") hash := sha256.Sum256([]byte(conversationData)) return fmt.Sprintf("%x", hash[:16]) // Use first 16 bytes for conversation hash } // normalizeMessageContent converts message content to a normalized string func (c *ConversationDiffAnalyzer) normalizeMessageContent(content interface{}) string { switch v := content.(type) { case string: return strings.TrimSpace(v) case []interface{}: var parts []string for _, item := range v { if block, ok := item.(map[string]interface{}); ok { if text, hasText := block["text"].(string); hasText { parts = append(parts, strings.TrimSpace(text)) } else if blockType, hasType := block["type"].(string); hasType { // Handle different content types (tool_use, etc.) switch blockType { case "tool_use": if name, hasName := block["name"].(string); hasName { parts = append(parts, fmt.Sprintf("TOOL:%s", name)) } case "tool_result": parts = append(parts, "TOOL_RESULT") default: parts = append(parts, fmt.Sprintf("CONTENT:%s", blockType)) } } } } return strings.Join(parts, " ") default: // Convert to JSON and back for normalization jsonBytes, _ := json.Marshal(content) return string(jsonBytes) } } // getMessagePreview creates a short preview of a message func (c *ConversationDiffAnalyzer) getMessagePreview(msg model.AnthropicMessage) string { content := c.normalizeMessageContent(msg.Content) if len(content) > 100 { return content[:100] + "..." } return content } // max returns the maximum of two integers func max(a, b int) int { if a > b { return a } return b }