// Package memory persists Claude's own freeform notes between runs — its // running thesis on positions, why past calls were made, and anything else // it wants available as context for future decisions. It is intentionally // just a flat text file: Claude rewrites it each run (Load hands back the // previous content, Save overwrites with the new content extracted from the // response), so summarization and pruning are Claude's job, not Go's. package memory import ( "fmt" "os" "path/filepath" "strings" "gitea.konchin.com/service/stonks/internal/config" ) const fileName = "memory.md" // Load reads back the memory file from cfg.Dir. Returns "" with no error if // it doesn't exist yet (e.g. first-ever run). func Load(cfg config.PortfolioConfig) (string, error) { dir, err := resolveDir(cfg.Dir) if err != nil { return "", err } data, err := os.ReadFile(filepath.Join(dir, fileName)) if err != nil { if os.IsNotExist(err) { return "", nil } return "", fmt.Errorf("memory: read: %w", err) } return string(data), nil } // Save overwrites the memory file under cfg.Dir with content. A blank // content is a no-op — it leaves whatever was there alone, rather than // wiping out prior notes if Claude's response happened to omit the section. func Save(cfg config.PortfolioConfig, content string) error { content = strings.TrimSpace(content) if content == "" { return nil } dir, err := resolveDir(cfg.Dir) if err != nil { return err } if err := os.MkdirAll(dir, 0o755); err != nil { return fmt.Errorf("memory: create %q: %w", dir, err) } if err := os.WriteFile(filepath.Join(dir, fileName), []byte(content+"\n"), 0o644); err != nil { return fmt.Errorf("memory: write: %w", err) } return nil } func resolveDir(dir string) (string, error) { if dir == "" { dir = "~/.local/state/stonks" } if dir != "~" && !strings.HasPrefix(dir, "~/") { return dir, nil } home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("memory: resolve home directory: %w", err) } if dir == "~" { return home, nil } return filepath.Join(home, dir[2:]), nil }