flake-update-20260505
  1// Package ai scans AI session history from ~/.local/share/ai/.
  2package ai
  3
  4import (
  5	"bufio"
  6	"os"
  7	"path/filepath"
  8	"regexp"
  9	"strings"
 10	"time"
 11)
 12
 13var (
 14	// **Date:** 2026-03-26
 15	mdDateRe = regexp.MustCompile(`^\*\*Date:\*\*\s+(\d{4}-\d{2}-\d{2})`)
 16	// **Tool:** pi
 17	mdToolRe = regexp.MustCompile(`^\*\*Tool:\*\*\s+(.+)`)
 18	// **Project:** vdemeester/home
 19	mdProjectRe = regexp.MustCompile(`^\*\*Project:\*\*\s+(.+)`)
 20	// # Title or # Some Title Here
 21	mdTitleRe = regexp.MustCompile(`^#\s+(.+)$`)
 22	// Filename: 2026-03-26-description.md
 23	filenameDateRe = regexp.MustCompile(`^(\d{4}-\d{2}-\d{2})`)
 24)
 25
 26// Item represents an AI session, learning, or research entry.
 27type Item struct {
 28	Title   string
 29	Type    string // "session", "learning", "research"
 30	Date    time.Time
 31	Tool    string
 32	Project string
 33	Path    string
 34}
 35
 36// FetchItemsSince scans AI storage directories for items within the time range.
 37func FetchItemsSince(baseDir string, start, end time.Time) ([]Item, error) {
 38	var items []Item
 39
 40	types := []struct {
 41		dir      string
 42		itemType string
 43	}{
 44		{"sessions", "session"},
 45		{"learnings", "learning"},
 46		{"research", "research"},
 47	}
 48
 49	for _, t := range types {
 50		dir := filepath.Join(baseDir, t.dir)
 51		fetched, err := scanDir(dir, t.itemType, start, end)
 52		if err != nil {
 53			continue
 54		}
 55		items = append(items, fetched...)
 56	}
 57
 58	return items, nil
 59}
 60
 61func scanDir(baseDir, itemType string, start, end time.Time) ([]Item, error) {
 62	resolved, err := filepath.EvalSymlinks(baseDir)
 63	if err != nil {
 64		resolved = baseDir
 65	}
 66
 67	var items []Item
 68
 69	err = filepath.Walk(resolved, func(path string, info os.FileInfo, err error) error {
 70		if err != nil || info.IsDir() {
 71			return nil
 72		}
 73
 74		ext := strings.ToLower(filepath.Ext(path))
 75		if ext != ".md" {
 76			return nil
 77		}
 78
 79		// Skip small files, export dirs
 80		if info.Size() < 50 || strings.Contains(path, "/exports/") {
 81			return nil
 82		}
 83
 84		// Quick date filter from filename before parsing
 85		basename := filepath.Base(path)
 86		if m := filenameDateRe.FindString(basename); m != "" {
 87			if d, err := time.Parse("2006-01-02", m); err == nil {
 88				if d.Before(start.Truncate(24*time.Hour)) || d.After(end) {
 89					return nil
 90				}
 91			}
 92		}
 93
 94		item, err := parseFile(path, itemType)
 95		if err != nil {
 96			return nil
 97		}
 98
 99		if item.Date.Before(start) || item.Date.After(end) {
100			return nil
101		}
102
103		items = append(items, *item)
104		return nil
105	})
106
107	return items, err
108}
109
110func parseFile(path, itemType string) (*Item, error) {
111	f, err := os.Open(path)
112	if err != nil {
113		return nil, err
114	}
115	defer f.Close()
116
117	item := &Item{
118		Type: itemType,
119		Path: path,
120	}
121
122	scanner := bufio.NewScanner(f)
123	lines := 0
124	for scanner.Scan() && lines < 30 {
125		line := scanner.Text()
126		lines++
127
128		if m := mdDateRe.FindStringSubmatch(line); len(m) > 1 && item.Date.IsZero() {
129			if d, err := time.Parse("2006-01-02", m[1]); err == nil {
130				item.Date = d
131			}
132		}
133		if m := mdToolRe.FindStringSubmatch(line); len(m) > 1 && item.Tool == "" {
134			item.Tool = strings.TrimSpace(m[1])
135		}
136		if m := mdProjectRe.FindStringSubmatch(line); len(m) > 1 && item.Project == "" {
137			item.Project = strings.TrimSpace(m[1])
138		}
139		if m := mdTitleRe.FindStringSubmatch(line); len(m) > 1 && item.Title == "" {
140			item.Title = strings.TrimSpace(m[1])
141		}
142	}
143
144	// Fallback title from filename
145	if item.Title == "" {
146		item.Title = cleanFilename(filepath.Base(path))
147	}
148
149	// Fallback date from filename
150	if item.Date.IsZero() {
151		if m := filenameDateRe.FindString(filepath.Base(path)); m != "" {
152			if d, err := time.Parse("2006-01-02", m); err == nil {
153				item.Date = d
154			}
155		}
156	}
157
158	return item, nil
159}
160
161func cleanFilename(name string) string {
162	name = strings.TrimSuffix(name, filepath.Ext(name))
163	name = filenameDateRe.ReplaceAllString(name, "")
164	name = strings.TrimPrefix(name, "-")
165	name = strings.ReplaceAll(name, "-", " ")
166	return strings.TrimSpace(name)
167}