mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-14 02:11:34 +00:00
Initial commit
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
// Package sdk provides SDK agent integration for claude-mnemonic.
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/lukaszraczylo/claude-mnemonic/pkg/models"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var (
|
||||
// Observation parsing
|
||||
observationRegex = regexp.MustCompile(`(?s)<observation>(.*?)</observation>`)
|
||||
|
||||
// Summary parsing
|
||||
summaryRegex = regexp.MustCompile(`(?s)<summary>(.*?)</summary>`)
|
||||
skipSummaryRegex = regexp.MustCompile(`<skip_summary\s+reason="([^"]+)"\s*/>`)
|
||||
|
||||
// Valid observation types
|
||||
validObsTypes = map[string]bool{
|
||||
"bugfix": true,
|
||||
"feature": true,
|
||||
"refactor": true,
|
||||
"change": true,
|
||||
"discovery": true,
|
||||
"decision": true,
|
||||
}
|
||||
|
||||
// Valid concepts (strict list - no custom tags allowed)
|
||||
validConcepts = map[string]bool{
|
||||
"how-it-works": true,
|
||||
"why-it-exists": true,
|
||||
"what-changed": true,
|
||||
"problem-solution": true,
|
||||
"gotcha": true,
|
||||
"pattern": true,
|
||||
"trade-off": true,
|
||||
}
|
||||
)
|
||||
|
||||
// ParseObservations parses observation XML blocks from SDK response text.
|
||||
func ParseObservations(text string, correlationID string) []*models.ParsedObservation {
|
||||
var observations []*models.ParsedObservation
|
||||
|
||||
matches := observationRegex.FindAllStringSubmatch(text, -1)
|
||||
for _, match := range matches {
|
||||
if len(match) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
obsContent := match[1]
|
||||
|
||||
// Extract fields
|
||||
obsType := extractField(obsContent, "type")
|
||||
title := extractField(obsContent, "title")
|
||||
subtitle := extractField(obsContent, "subtitle")
|
||||
narrative := extractField(obsContent, "narrative")
|
||||
facts := extractArrayElements(obsContent, "facts", "fact")
|
||||
concepts := extractArrayElements(obsContent, "concepts", "concept")
|
||||
filesRead := extractArrayElements(obsContent, "files_read", "file")
|
||||
filesModified := extractArrayElements(obsContent, "files_modified", "file")
|
||||
|
||||
// Determine final type (default to "change" if invalid)
|
||||
finalType := models.ObsTypeChange
|
||||
if obsType != "" {
|
||||
if validObsTypes[obsType] {
|
||||
finalType = models.ObservationType(obsType)
|
||||
} else {
|
||||
log.Warn().
|
||||
Str("correlationId", correlationID).
|
||||
Str("invalidType", obsType).
|
||||
Msg("Invalid observation type, using 'change'")
|
||||
}
|
||||
} else {
|
||||
log.Warn().
|
||||
Str("correlationId", correlationID).
|
||||
Msg("Observation missing type field, using 'change'")
|
||||
}
|
||||
|
||||
// Filter concepts: only keep valid ones from the strict list
|
||||
cleanedConcepts := make([]string, 0, len(concepts))
|
||||
var invalidConcepts []string
|
||||
for _, c := range concepts {
|
||||
c = strings.ToLower(strings.TrimSpace(c))
|
||||
if c == string(finalType) {
|
||||
continue // Skip type in concepts
|
||||
}
|
||||
if validConcepts[c] {
|
||||
cleanedConcepts = append(cleanedConcepts, c)
|
||||
} else {
|
||||
invalidConcepts = append(invalidConcepts, c)
|
||||
}
|
||||
}
|
||||
if len(invalidConcepts) > 0 {
|
||||
log.Warn().
|
||||
Str("correlationId", correlationID).
|
||||
Strs("invalidConcepts", invalidConcepts).
|
||||
Msg("Filtered out invalid concepts (not in allowed list)")
|
||||
}
|
||||
|
||||
observations = append(observations, &models.ParsedObservation{
|
||||
Type: finalType,
|
||||
Title: title,
|
||||
Subtitle: subtitle,
|
||||
Facts: facts,
|
||||
Narrative: narrative,
|
||||
Concepts: cleanedConcepts,
|
||||
FilesRead: filesRead,
|
||||
FilesModified: filesModified,
|
||||
})
|
||||
}
|
||||
|
||||
return observations
|
||||
}
|
||||
|
||||
// ParseSummary parses a summary XML block from SDK response text.
|
||||
func ParseSummary(text string, sessionID int64) *models.ParsedSummary {
|
||||
// Check for skip_summary first
|
||||
if skipMatch := skipSummaryRegex.FindStringSubmatch(text); skipMatch != nil {
|
||||
log.Info().
|
||||
Int64("sessionId", sessionID).
|
||||
Str("reason", skipMatch[1]).
|
||||
Msg("Summary skipped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find summary block
|
||||
match := summaryRegex.FindStringSubmatch(text)
|
||||
if len(match) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
summaryContent := match[1]
|
||||
|
||||
return &models.ParsedSummary{
|
||||
Request: extractField(summaryContent, "request"),
|
||||
Investigated: extractField(summaryContent, "investigated"),
|
||||
Learned: extractField(summaryContent, "learned"),
|
||||
Completed: extractField(summaryContent, "completed"),
|
||||
NextSteps: extractField(summaryContent, "next_steps"),
|
||||
Notes: extractField(summaryContent, "notes"),
|
||||
}
|
||||
}
|
||||
|
||||
// extractField extracts a simple field value from XML content.
|
||||
func extractField(content, fieldName string) string {
|
||||
pattern := regexp.MustCompile(`<` + fieldName + `>([^<]*)</` + fieldName + `>`)
|
||||
match := pattern.FindStringSubmatch(content)
|
||||
if len(match) < 2 {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(match[1])
|
||||
}
|
||||
|
||||
// extractArrayElements extracts array elements from XML content.
|
||||
func extractArrayElements(content, arrayName, elementName string) []string {
|
||||
var elements []string
|
||||
|
||||
// Find the array block
|
||||
arrayPattern := regexp.MustCompile(`(?s)<` + arrayName + `>(.*?)</` + arrayName + `>`)
|
||||
arrayMatch := arrayPattern.FindStringSubmatch(content)
|
||||
if len(arrayMatch) < 2 {
|
||||
return elements
|
||||
}
|
||||
|
||||
arrayContent := arrayMatch[1]
|
||||
|
||||
// Extract individual elements
|
||||
elementPattern := regexp.MustCompile(`<` + elementName + `>([^<]+)</` + elementName + `>`)
|
||||
elementMatches := elementPattern.FindAllStringSubmatch(arrayContent, -1)
|
||||
for _, match := range elementMatches {
|
||||
if len(match) >= 2 {
|
||||
elements = append(elements, strings.TrimSpace(match[1]))
|
||||
}
|
||||
}
|
||||
|
||||
return elements
|
||||
}
|
||||
Reference in New Issue
Block a user