mirror of
https://github.com/imjasonh/cnotes
synced 2026-07-08 17:16:00 +00:00
Add comprehensive documentation for the git notes functionality: - Example usage and real-world scenarios - Commands for viewing conversation context attached to commits - Configuration options via .claude/notes.json - Privacy controls and sensitive data filtering - Team collaboration workflows for sharing git notes - Complete JSON example showing structured conversation data Users can now easily understand how to view and configure the automatic conversation context attachment to their git commits. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
241 lines
7.1 KiB
Go
241 lines
7.1 KiB
Go
package context
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// ConversationContext represents relevant conversation context for a commit
|
|
type ConversationContext struct {
|
|
UserPrompts []string `json:"user_prompts"`
|
|
ClaudeResponses []string `json:"claude_responses"`
|
|
ToolInteractions []ToolInteraction `json:"tool_interactions"`
|
|
}
|
|
|
|
// ToolInteraction represents a tool use and its result
|
|
type ToolInteraction struct {
|
|
Tool string `json:"tool"`
|
|
Input string `json:"input"`
|
|
Output string `json:"output"`
|
|
Duration string `json:"duration,omitempty"`
|
|
}
|
|
|
|
// ContextExtractor extracts relevant conversation context from transcripts
|
|
type ContextExtractor struct {
|
|
maxExcerptLength int
|
|
sensitivePatterns []*regexp.Regexp
|
|
}
|
|
|
|
// NewContextExtractor creates a new context extractor with default settings
|
|
func NewContextExtractor() *ContextExtractor {
|
|
// Patterns to filter out sensitive information
|
|
sensitivePatterns := []*regexp.Regexp{
|
|
regexp.MustCompile(`(?i)(password|token|key|secret)[:\s]*[^\s\n]+`),
|
|
regexp.MustCompile(`(?i)(api[_-]?key)[:\s]*[^\s\n]+`),
|
|
regexp.MustCompile(`-----BEGIN [A-Z ]+-----`), // Private keys
|
|
regexp.MustCompile(`[A-Za-z0-9+/]{40,}={0,2}`), // Base64 encoded secrets
|
|
}
|
|
|
|
return &ContextExtractor{
|
|
maxExcerptLength: 5000, // 5KB limit for context
|
|
sensitivePatterns: sensitivePatterns,
|
|
}
|
|
}
|
|
|
|
// ExtractRecentContext extracts recent conversation context from a transcript file
|
|
func (ce *ContextExtractor) ExtractRecentContext(transcriptPath string, sessionID string) (*ConversationContext, error) {
|
|
if transcriptPath == "" {
|
|
return &ConversationContext{}, nil
|
|
}
|
|
|
|
file, err := os.Open(transcriptPath)
|
|
if err != nil {
|
|
// Transcript might not exist, return empty context
|
|
return &ConversationContext{}, nil
|
|
}
|
|
defer file.Close()
|
|
|
|
content, err := io.ReadAll(file)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read transcript: %w", err)
|
|
}
|
|
|
|
// Parse the transcript content
|
|
context := ce.parseTranscriptContent(string(content), sessionID)
|
|
|
|
// Apply privacy filters
|
|
context = ce.filterSensitiveContent(context)
|
|
|
|
return context, nil
|
|
}
|
|
|
|
// parseTranscriptContent parses transcript content and extracts conversation elements
|
|
func (ce *ContextExtractor) parseTranscriptContent(content, sessionID string) *ConversationContext {
|
|
context := &ConversationContext{
|
|
UserPrompts: []string{},
|
|
ClaudeResponses: []string{},
|
|
ToolInteractions: []ToolInteraction{},
|
|
}
|
|
|
|
lines := strings.Split(content, "\n")
|
|
currentMessage := ""
|
|
messageType := ""
|
|
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
|
|
// Skip empty lines
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
// Detect message boundaries and types
|
|
if strings.HasPrefix(line, "Human:") || strings.HasPrefix(line, "User:") {
|
|
// Save previous message if exists
|
|
if currentMessage != "" && messageType == "claude" {
|
|
context.ClaudeResponses = append(context.ClaudeResponses, strings.TrimSpace(currentMessage))
|
|
}
|
|
|
|
messageType = "human"
|
|
currentMessage = strings.TrimPrefix(strings.TrimPrefix(line, "Human:"), "User:")
|
|
} else if strings.HasPrefix(line, "Assistant:") || strings.HasPrefix(line, "Claude:") {
|
|
// Save previous message if exists
|
|
if currentMessage != "" && messageType == "human" {
|
|
context.UserPrompts = append(context.UserPrompts, strings.TrimSpace(currentMessage))
|
|
}
|
|
|
|
messageType = "claude"
|
|
currentMessage = strings.TrimPrefix(strings.TrimPrefix(line, "Assistant:"), "Claude:")
|
|
} else if strings.Contains(line, "tool_use") || strings.Contains(line, "function_calls") {
|
|
// Try to parse tool interactions
|
|
toolInteraction := ce.parseToolInteraction(line)
|
|
if toolInteraction != nil {
|
|
context.ToolInteractions = append(context.ToolInteractions, *toolInteraction)
|
|
}
|
|
} else {
|
|
// Continue building current message
|
|
if currentMessage != "" {
|
|
currentMessage += "\n"
|
|
}
|
|
currentMessage += line
|
|
}
|
|
}
|
|
|
|
// Don't forget the last message
|
|
if currentMessage != "" {
|
|
if messageType == "human" {
|
|
context.UserPrompts = append(context.UserPrompts, strings.TrimSpace(currentMessage))
|
|
} else if messageType == "claude" {
|
|
context.ClaudeResponses = append(context.ClaudeResponses, strings.TrimSpace(currentMessage))
|
|
}
|
|
}
|
|
|
|
return context
|
|
}
|
|
|
|
// parseToolInteraction attempts to parse tool interaction from a line
|
|
func (ce *ContextExtractor) parseToolInteraction(line string) *ToolInteraction {
|
|
// This is a simplified parser - in reality, the transcript format may vary
|
|
if strings.Contains(line, "Bash") && strings.Contains(line, "command") {
|
|
return &ToolInteraction{
|
|
Tool: "Bash",
|
|
Input: ce.extractBetween(line, "command", "description"),
|
|
Output: "Command executed",
|
|
}
|
|
}
|
|
|
|
if strings.Contains(line, "Edit") || strings.Contains(line, "Write") {
|
|
return &ToolInteraction{
|
|
Tool: "File",
|
|
Input: ce.extractBetween(line, "file_path", "content"),
|
|
Output: "File modified",
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// extractBetween extracts text between two markers
|
|
func (ce *ContextExtractor) extractBetween(text, start, end string) string {
|
|
startIdx := strings.Index(text, start)
|
|
if startIdx == -1 {
|
|
return ""
|
|
}
|
|
|
|
endIdx := strings.Index(text[startIdx:], end)
|
|
if endIdx == -1 {
|
|
return text[startIdx:]
|
|
}
|
|
|
|
return text[startIdx : startIdx+endIdx]
|
|
}
|
|
|
|
// filterSensitiveContent removes sensitive information from context
|
|
func (ce *ContextExtractor) filterSensitiveContent(context *ConversationContext) *ConversationContext {
|
|
// Filter user prompts
|
|
for i, prompt := range context.UserPrompts {
|
|
context.UserPrompts[i] = ce.sanitizeText(prompt)
|
|
}
|
|
|
|
// Filter Claude responses
|
|
for i, response := range context.ClaudeResponses {
|
|
context.ClaudeResponses[i] = ce.sanitizeText(response)
|
|
}
|
|
|
|
// Filter tool interactions
|
|
for i, interaction := range context.ToolInteractions {
|
|
context.ToolInteractions[i].Input = ce.sanitizeText(interaction.Input)
|
|
context.ToolInteractions[i].Output = ce.sanitizeText(interaction.Output)
|
|
}
|
|
|
|
return context
|
|
}
|
|
|
|
// sanitizeText removes sensitive patterns from text
|
|
func (ce *ContextExtractor) sanitizeText(text string) string {
|
|
for _, pattern := range ce.sensitivePatterns {
|
|
text = pattern.ReplaceAllString(text, "[REDACTED]")
|
|
}
|
|
return text
|
|
}
|
|
|
|
// CreateExcerpt creates a concise excerpt from conversation context
|
|
func (ce *ContextExtractor) CreateExcerpt(context *ConversationContext) string {
|
|
var parts []string
|
|
|
|
// Include recent user prompts (last 2)
|
|
if len(context.UserPrompts) > 0 {
|
|
start := len(context.UserPrompts) - 2
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
parts = append(parts, "Recent user prompts:")
|
|
for i := start; i < len(context.UserPrompts); i++ {
|
|
prompt := context.UserPrompts[i]
|
|
if len(prompt) > 200 {
|
|
prompt = prompt[:197] + "..."
|
|
}
|
|
parts = append(parts, fmt.Sprintf("- %s", prompt))
|
|
}
|
|
}
|
|
|
|
// Include recent tool interactions
|
|
if len(context.ToolInteractions) > 0 {
|
|
parts = append(parts, "\nTool interactions:")
|
|
for _, interaction := range context.ToolInteractions {
|
|
parts = append(parts, fmt.Sprintf("- %s: %s", interaction.Tool, interaction.Input))
|
|
}
|
|
}
|
|
|
|
excerpt := strings.Join(parts, "\n")
|
|
|
|
// Truncate if too long
|
|
if len(excerpt) > ce.maxExcerptLength {
|
|
excerpt = excerpt[:ce.maxExcerptLength-3] + "..."
|
|
}
|
|
|
|
return excerpt
|
|
}
|