Commit 44ae208602b1
Changed files (4)
tools
daily-plan
cmd
daily-plan
internal
tools/daily-plan/cmd/daily-plan/main.go
@@ -17,6 +17,7 @@ import (
"strings"
"time"
+ "github.com/vdemeester/home/tools/daily-plan/internal/ai"
"github.com/vdemeester/home/tools/daily-plan/internal/cache"
"github.com/vdemeester/home/tools/daily-plan/internal/config"
"github.com/vdemeester/home/tools/daily-plan/internal/display"
@@ -188,6 +189,20 @@ func ghToJSON(items []github.Item) []jsonGH {
return out
}
+func aiToJSON(items []ai.Item) []jsonAI {
+ result := make([]jsonAI, 0, len(items))
+ for _, item := range items {
+ result = append(result, jsonAI{
+ Title: item.Title,
+ Type: item.Type,
+ Date: item.Date.Format("2006-01-02"),
+ Tool: item.Tool,
+ Project: item.Project,
+ })
+ }
+ return result
+}
+
func orgDoneToJSON(items []org.DoneItem) []jsonOrgDone {
result := make([]jsonOrgDone, 0, len(items))
for _, d := range items {
@@ -539,10 +554,19 @@ type jsonOrgDone struct {
CompletedAt string `json:"completed_at"`
}
+type jsonAI struct {
+ Title string `json:"title"`
+ Type string `json:"type"`
+ Date string `json:"date"`
+ Tool string `json:"tool,omitempty"`
+ Project string `json:"project,omitempty"`
+}
+
type jsonWeekly struct {
Week string `json:"week"`
NextMonday string `json:"next_monday"`
OrgDone []jsonOrgDone `json:"org_done,omitempty"`
+ AISessions []jsonAI `json:"ai_sessions,omitempty"`
JiraCompleted []jsonJira `json:"jira_completed"`
GHMerged []jsonGH `json:"github_merged"`
GHReviewed []jsonGH `json:"github_reviewed"`
@@ -589,6 +613,10 @@ func cmdWeekly(ctx context.Context, cfg *config.Config) error {
return org.FetchDoneItems(cfg.Org.Files, cfg.Org.ArchiveDir, monday, nextMonday)
})
+ aiItems, _ := cache.GetOrFetch(apiCache, "ai:items:"+mondayStr, func() ([]ai.Item, error) {
+ return ai.FetchItemsSince(cfg.AI.Dir, monday, nextMonday)
+ })
+
var discussions []github.DiscussionItem
var comments []github.CommentItem
if !noDiscussions {
@@ -607,6 +635,7 @@ func cmdWeekly(ctx context.Context, cfg *config.Config) error {
Week: monday.Format("2006-01-02"),
NextMonday: nextMonday.Format("2006-01-02"),
OrgDone: orgDoneToJSON(orgDone),
+ AISessions: aiToJSON(aiItems),
JiraCompleted: jiraToJSON(completed, cfg.Jira.BaseURL),
GHMerged: ghToJSON(merged),
GHReviewed: ghToJSON(reviewed),
@@ -631,6 +660,11 @@ func cmdWeekly(ctx context.Context, cfg *config.Config) error {
display.OrgDoneItems(orgDone)
}
+ if len(aiItems) > 0 {
+ display.SubHeader("AI Sessions This Week")
+ display.AIItems(aiItems)
+ }
+
display.SubHeader("Completed This Week (Jira)")
display.JiraIssues(completed, "done")
tools/daily-plan/internal/ai/ai.go
@@ -0,0 +1,167 @@
+// Package ai scans AI session history from ~/.local/share/ai/.
+package ai
+
+import (
+ "bufio"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "time"
+)
+
+var (
+ // **Date:** 2026-03-26
+ mdDateRe = regexp.MustCompile(`^\*\*Date:\*\*\s+(\d{4}-\d{2}-\d{2})`)
+ // **Tool:** pi
+ mdToolRe = regexp.MustCompile(`^\*\*Tool:\*\*\s+(.+)`)
+ // **Project:** vdemeester/home
+ mdProjectRe = regexp.MustCompile(`^\*\*Project:\*\*\s+(.+)`)
+ // # Title or # Some Title Here
+ mdTitleRe = regexp.MustCompile(`^#\s+(.+)$`)
+ // Filename: 2026-03-26-description.md
+ filenameDateRe = regexp.MustCompile(`^(\d{4}-\d{2}-\d{2})`)
+)
+
+// Item represents an AI session, learning, or research entry.
+type Item struct {
+ Title string
+ Type string // "session", "learning", "research"
+ Date time.Time
+ Tool string
+ Project string
+ Path string
+}
+
+// FetchItemsSince scans AI storage directories for items within the time range.
+func FetchItemsSince(baseDir string, start, end time.Time) ([]Item, error) {
+ var items []Item
+
+ types := []struct {
+ dir string
+ itemType string
+ }{
+ {"sessions", "session"},
+ {"learnings", "learning"},
+ {"research", "research"},
+ }
+
+ for _, t := range types {
+ dir := filepath.Join(baseDir, t.dir)
+ fetched, err := scanDir(dir, t.itemType, start, end)
+ if err != nil {
+ continue
+ }
+ items = append(items, fetched...)
+ }
+
+ return items, nil
+}
+
+func scanDir(baseDir, itemType string, start, end time.Time) ([]Item, error) {
+ resolved, err := filepath.EvalSymlinks(baseDir)
+ if err != nil {
+ resolved = baseDir
+ }
+
+ var items []Item
+
+ err = filepath.Walk(resolved, func(path string, info os.FileInfo, err error) error {
+ if err != nil || info.IsDir() {
+ return nil
+ }
+
+ ext := strings.ToLower(filepath.Ext(path))
+ if ext != ".md" {
+ return nil
+ }
+
+ // Skip small files, export dirs
+ if info.Size() < 50 || strings.Contains(path, "/exports/") {
+ return nil
+ }
+
+ // Quick date filter from filename before parsing
+ basename := filepath.Base(path)
+ if m := filenameDateRe.FindString(basename); m != "" {
+ if d, err := time.Parse("2006-01-02", m); err == nil {
+ if d.Before(start.Truncate(24*time.Hour)) || d.After(end) {
+ return nil
+ }
+ }
+ }
+
+ item, err := parseFile(path, itemType)
+ if err != nil {
+ return nil
+ }
+
+ if item.Date.Before(start) || item.Date.After(end) {
+ return nil
+ }
+
+ items = append(items, *item)
+ return nil
+ })
+
+ return items, err
+}
+
+func parseFile(path, itemType string) (*Item, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ item := &Item{
+ Type: itemType,
+ Path: path,
+ }
+
+ scanner := bufio.NewScanner(f)
+ lines := 0
+ for scanner.Scan() && lines < 30 {
+ line := scanner.Text()
+ lines++
+
+ if m := mdDateRe.FindStringSubmatch(line); len(m) > 1 && item.Date.IsZero() {
+ if d, err := time.Parse("2006-01-02", m[1]); err == nil {
+ item.Date = d
+ }
+ }
+ if m := mdToolRe.FindStringSubmatch(line); len(m) > 1 && item.Tool == "" {
+ item.Tool = strings.TrimSpace(m[1])
+ }
+ if m := mdProjectRe.FindStringSubmatch(line); len(m) > 1 && item.Project == "" {
+ item.Project = strings.TrimSpace(m[1])
+ }
+ if m := mdTitleRe.FindStringSubmatch(line); len(m) > 1 && item.Title == "" {
+ item.Title = strings.TrimSpace(m[1])
+ }
+ }
+
+ // Fallback title from filename
+ if item.Title == "" {
+ item.Title = cleanFilename(filepath.Base(path))
+ }
+
+ // Fallback date from filename
+ if item.Date.IsZero() {
+ if m := filenameDateRe.FindString(filepath.Base(path)); m != "" {
+ if d, err := time.Parse("2006-01-02", m); err == nil {
+ item.Date = d
+ }
+ }
+ }
+
+ return item, nil
+}
+
+func cleanFilename(name string) string {
+ name = strings.TrimSuffix(name, filepath.Ext(name))
+ name = filenameDateRe.ReplaceAllString(name, "")
+ name = strings.TrimPrefix(name, "-")
+ name = strings.ReplaceAll(name, "-", " ")
+ return strings.TrimSpace(name)
+}
tools/daily-plan/internal/config/config.go
@@ -17,10 +17,18 @@ type Config struct {
// Org-mode configuration
Org OrgConfig
+ // AI session history configuration
+ AI AIConfig
+
// State directory for last-check tracking
StateDir string
}
+// AIConfig configures AI session history scanning.
+type AIConfig struct {
+ Dir string // Base directory (e.g. ~/.local/share/ai)
+}
+
// JiraConfig configures Jira integration.
type JiraConfig struct {
User string // Jira username (e.g. "vdemeest")
@@ -85,6 +93,9 @@ func DefaultConfig() *Config {
},
ArchiveDir: filepath.Join(home, "desktop", "org", "archive"),
},
+ AI: AIConfig{
+ Dir: filepath.Join(home, ".local", "share", "ai"),
+ },
StateDir: filepath.Join(home, ".local", "share", "daily-plan"),
}
}
tools/daily-plan/internal/display/display.go
@@ -5,6 +5,7 @@ import (
"fmt"
"strings"
+ "github.com/vdemeester/home/tools/daily-plan/internal/ai"
"github.com/vdemeester/home/tools/daily-plan/internal/github"
"github.com/vdemeester/home/tools/daily-plan/internal/jira"
"github.com/vdemeester/home/tools/daily-plan/internal/org"
@@ -230,6 +231,91 @@ func OrgDoneItems(items []org.DoneItem) {
}
}
+// AIItems prints AI session/learning/research items grouped by type.
+// Sessions are shown as a compact summary (count per day + notable titles).
+// Learnings and research are shown individually.
+func AIItems(items []ai.Item) {
+ if len(items) == 0 {
+ fmt.Printf(" %s(none)%s\n", dim, reset)
+ return
+ }
+ // Separate by type
+ var sessions, learnings, research []ai.Item
+ for _, item := range items {
+ // Skip auto-recovered noise
+ if strings.Contains(strings.ToLower(item.Title), "auto-recovered") {
+ continue
+ }
+ switch item.Type {
+ case "session":
+ sessions = append(sessions, item)
+ case "learning":
+ learnings = append(learnings, item)
+ case "research":
+ research = append(research, item)
+ }
+ }
+
+ if len(sessions) > 0 {
+ // Group sessions by date and show counts + notable titles
+ byDate := make(map[string][]ai.Item)
+ var dates []string
+ for _, s := range sessions {
+ d := s.Date.Format("2006-01-02")
+ if _, exists := byDate[d]; !exists {
+ dates = append(dates, d)
+ }
+ byDate[d] = append(byDate[d], s)
+ }
+ fmt.Printf(" %s%sSessions (%d total)%s\n", dim, bold, len(sessions), reset)
+ for _, d := range dates {
+ daySessions := byDate[d]
+ // Show up to 3 notable titles per day
+ fmt.Printf(" %s%s%s — %d sessions\n", cyan, d, reset, len(daySessions))
+ shown := 0
+ for _, s := range daySessions {
+ if shown >= 3 {
+ remaining := len(daySessions) - shown
+ if remaining > 0 {
+ fmt.Printf(" %s... and %d more%s\n", dim, remaining, reset)
+ }
+ break
+ }
+ title := s.Title
+ // Strip "Session: " prefix if present
+ title = strings.TrimPrefix(title, "Session: ")
+ if len(title) > 55 {
+ title = title[:52] + "..."
+ }
+ project := ""
+ if s.Project != "" {
+ project = fmt.Sprintf(" %s[%s]%s", dim, s.Project, reset)
+ }
+ fmt.Printf(" • %s%s\n", title, project)
+ shown++
+ }
+ }
+ }
+
+ printAIList := func(label string, items []ai.Item) {
+ if len(items) == 0 {
+ return
+ }
+ fmt.Printf(" %s%s%s (%d)%s\n", dim, bold, label, len(items), reset)
+ for _, item := range items {
+ title := item.Title
+ if len(title) > 60 {
+ title = title[:57] + "..."
+ }
+ fmt.Printf(" %s•%s %s %s%s%s\n",
+ cyan, reset, title, dim, item.Date.Format("2006-01-02"), reset)
+ }
+ }
+
+ printAIList("Learnings", learnings)
+ printAIList("Research", research)
+}
+
// DiscussionItems prints GitHub discussions.
func DiscussionItems(items []github.DiscussionItem) {
if len(items) == 0 {