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}