diff --git a/.claude/notes.json b/.claude/notes.json new file mode 100644 index 0000000..822d7bd --- /dev/null +++ b/.claude/notes.json @@ -0,0 +1,9 @@ +{ + "enabled": true, + "max_excerpt_length": 5000, + "max_prompts": 2, + "notes_ref": "claude-conversations", + "exclude_patterns": ["password", "token", "key", "secret"], + "user_emoji": "🧑", + "assistant_emoji": "🤖" +} \ No newline at end of file diff --git a/README.md b/README.md index afad772..f222f3a 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ ⭐️ `cnotes` is pronounced like 💵 c-notes or 🏊 [_cenotes_](https://en.wikipedia.org/wiki/Cenote) +The conversation display now includes configurable emojis for user and assistant messages. + ## Quick Start ```bash diff --git a/cmd/notes.go b/cmd/notes.go index 221e493..4d38b6a 100644 --- a/cmd/notes.go +++ b/cmd/notes.go @@ -6,6 +6,7 @@ import ( "os/exec" "strings" + "github.com/imjasonh/cnotes/internal/config" "github.com/imjasonh/cnotes/internal/notes" "github.com/spf13/cobra" ) @@ -101,7 +102,8 @@ If no commit is specified, shows notes for HEAD.`, } // Pretty-print in Markdown format - printConversationMarkdown(*note, commit) + cfg := config.LoadNotesConfig(".") + printConversationMarkdown(*note, commit, cfg) return nil }, } @@ -138,7 +140,7 @@ var listCmd = &cobra.Command{ } // printConversationMarkdown formats a conversation note as readable Markdown -func printConversationMarkdown(note notes.ConversationNote, commit string) { +func printConversationMarkdown(note notes.ConversationNote, commit string, cfg *config.NotesConfig) { fmt.Printf("# Claude Conversation Notes\n\n") // Get commit info @@ -155,7 +157,7 @@ func printConversationMarkdown(note notes.ConversationNote, commit string) { if note.ConversationExcerpt != "" { fmt.Printf("## Conversation Transcript\n\n") // Clean up and format the conversation excerpt for better readability - formatted := formatConversationExcerpt(note.ConversationExcerpt) + formatted := formatConversationExcerpt(note.ConversationExcerpt, cfg) fmt.Printf("%s\n\n", formatted) } @@ -164,7 +166,7 @@ func printConversationMarkdown(note notes.ConversationNote, commit string) { } // formatConversationExcerpt cleans up the conversation excerpt for better readability -func formatConversationExcerpt(excerpt string) string { +func formatConversationExcerpt(excerpt string, cfg *config.NotesConfig) string { // Replace escaped newlines with actual newlines formatted := strings.ReplaceAll(excerpt, "\\n", "\n") @@ -195,11 +197,11 @@ func formatConversationExcerpt(excerpt string) string { } switch { - case strings.HasPrefix(line, "User:"): + case strings.HasPrefix(line, "User:") || (cfg != nil && cfg.UserEmoji != "" && strings.HasPrefix(line, cfg.UserEmoji)): // Bold user prompts formattedLines = append(formattedLines, "**"+line+"**") - case strings.HasPrefix(line, "Claude:"): + case strings.HasPrefix(line, "Claude:") || (cfg != nil && cfg.AssistantEmoji != "" && strings.HasPrefix(line, cfg.AssistantEmoji)): // Keep Claude responses as-is formattedLines = append(formattedLines, line) diff --git a/cmd/root.go b/cmd/root.go index dee0482..c611eab 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -132,7 +132,7 @@ func processGitCommit(ctx context.Context, input HookInput, bashInput BashToolIn return fmt.Errorf("could not extract commit hash") } - // Create notes manager + // Create notes manager and load config notesManager := notes.NewNotesManager(input.CWD) cfg := config.LoadNotesConfig(input.CWD) notesManager.SetNotesRef(cfg.NotesRef) @@ -149,7 +149,7 @@ func processGitCommit(ctx context.Context, input HookInput, bashInput BashToolIn time.Sleep(100 * time.Millisecond) // Extract conversation context since the last commit - contextExtractor := conv.NewContextExtractor() + contextExtractor := conv.NewContextExtractor(cfg) conversationContext, err := contextExtractor.ExtractContextSince(input.TranscriptPath, input.SessionID, previousCommitTime) if err != nil { return fmt.Errorf("failed to extract conversation context: %w", err) diff --git a/internal/config/notes.go b/internal/config/notes.go index bf9e82d..8a7af9b 100644 --- a/internal/config/notes.go +++ b/internal/config/notes.go @@ -14,6 +14,8 @@ type NotesConfig struct { IncludeToolOutput bool `json:"include_tool_output"` // Whether to include tool output in notes NotesRef string `json:"notes_ref"` // Git notes reference name ExcludePatterns []string `json:"exclude_patterns"` // Patterns to exclude from notes + UserEmoji string `json:"user_emoji"` // Emoji to use for user messages + AssistantEmoji string `json:"assistant_emoji"` // Emoji to use for assistant messages } // DefaultNotesConfig returns the default configuration @@ -32,6 +34,8 @@ func DefaultNotesConfig() *NotesConfig { "api_key", "auth", }, + UserEmoji: "👤", + AssistantEmoji: "🤖", } } @@ -62,6 +66,12 @@ func LoadNotesConfig(projectDir string) *NotesConfig { if config.MaxPrompts <= 0 { config.MaxPrompts = 2 } + if config.UserEmoji == "" { + config.UserEmoji = "👤" + } + if config.AssistantEmoji == "" { + config.AssistantEmoji = "🤖" + } return &config } diff --git a/internal/context/conversation.go b/internal/context/conversation.go index e3a9042..9f83da3 100644 --- a/internal/context/conversation.go +++ b/internal/context/conversation.go @@ -10,6 +10,8 @@ import ( "sort" "strings" "time" + + "github.com/imjasonh/cnotes/internal/config" ) // ConversationContext represents relevant conversation context for a commit @@ -40,10 +42,11 @@ type ToolInteraction struct { type ContextExtractor struct { maxExcerptLength int sensitivePatterns []*regexp.Regexp + config *config.NotesConfig } // NewContextExtractor creates a new context extractor with default settings -func NewContextExtractor() *ContextExtractor { +func NewContextExtractor(cfg *config.NotesConfig) *ContextExtractor { // Patterns to filter out sensitive information sensitivePatterns := []*regexp.Regexp{ regexp.MustCompile(`(?i)(password|token|key|secret)[:\s]*[^\s\n]+`), @@ -52,9 +55,15 @@ func NewContextExtractor() *ContextExtractor { regexp.MustCompile(`[A-Za-z0-9+/]{40,}={0,2}`), // Base64 encoded secrets } + maxLength := 5000 + if cfg != nil && cfg.MaxExcerptLength > 0 { + maxLength = cfg.MaxExcerptLength + } + return &ContextExtractor{ - maxExcerptLength: 5000, // 5KB limit for context + maxExcerptLength: maxLength, sensitivePatterns: sensitivePatterns, + config: cfg, } } @@ -361,7 +370,11 @@ func (ce *ContextExtractor) CreateExcerpt(context *ConversationContext) string { if len(content) > 200 { content = content[:197] + "..." } - line = fmt.Sprintf("User: %s", content) + emoji := "👤" + if ce.config != nil && ce.config.UserEmoji != "" { + emoji = ce.config.UserEmoji + } + line = fmt.Sprintf("%s User: %s", emoji, content) case "assistant": // Format assistant responses @@ -369,7 +382,11 @@ func (ce *ContextExtractor) CreateExcerpt(context *ConversationContext) string { if len(content) > 200 { content = content[:197] + "..." } - line = fmt.Sprintf("Claude: %s", content) + emoji := "🤖" + if ce.config != nil && ce.config.AssistantEmoji != "" { + emoji = ce.config.AssistantEmoji + } + line = fmt.Sprintf("%s Claude: %s", emoji, content) case "tool": // Format tool uses