cmd/llm_aggregator.go (view raw)
1package main
2
3import (
4 "context"
5 "fmt"
6 "os"
7
8 tea "github.com/charmbracelet/bubbletea"
9 "llm_aggregator/internal/cli"
10 "llm_aggregator/internal/config"
11 "llm_aggregator/internal/progress"
12 "llm_aggregator/internal/runtime"
13 "llm_aggregator/internal/tui"
14)
15
16var (
17 version string
18 buildDate string
19)
20
21func main() {
22 cli.BuildDate = buildDate
23 cli.Version = version
24
25 // Load configuration from file and environment variables
26 cfg, err := config.Load()
27 if err != nil {
28 fmt.Fprintf(os.Stderr, "Warning: could not load configuration: %v\n", err)
29 // Continue with defaults
30 cfg = config.DefaultConfig()
31 }
32
33 // Parse command line arguments
34 args, err := cli.ParseArgs()
35 if err != nil {
36 fmt.Fprintf(os.Stderr, "Error parsing arguments: %v\n", err)
37 os.Exit(1)
38 }
39
40 // Create runtime configuration
41 rt := runtime.NewRuntime()
42 rt.FeedsFile = args.FeedsFile
43 rt.Prompt = args.Prompt
44
45 // Apply configuration with precedence: CLI args > Environment vars > Config file > Defaults
46 applyConfiguration(rt, cfg, args)
47
48 // Validate API key
49 if rt.APIKey == "" {
50 fmt.Fprintln(os.Stderr, "Error: OpenAI-compatible API key is required. Set via --api-key, LLM_AGGREGATOR_API_KEY environment variable, or config file.")
51 os.Exit(1)
52 }
53
54 // Run with TUI if requested
55 if args.TUI {
56 runWithTUI(rt)
57 } else {
58 runWithoutTUI(rt, args.Verbose)
59 }
60}
61
62// applyConfiguration applies configuration with proper precedence
63func applyConfiguration(rt *runtime.Runtime, cfg *config.Config, args *cli.Args) {
64 // Feed aggregation options - CLI args override config
65 if args.MaxArticlesPerFeed != 0 {
66 rt.MaxArticlesPerFeed = args.MaxArticlesPerFeed
67 } else if cfg.MaxArticlesPerFeed != 0 {
68 rt.MaxArticlesPerFeed = cfg.MaxArticlesPerFeed
69 }
70
71 if args.MaxDaysOld != 0 {
72 rt.MaxDaysOld = args.MaxDaysOld
73 } else if cfg.MaxDaysOld != 0 {
74 rt.MaxDaysOld = cfg.MaxDaysOld
75 }
76
77 if args.MaxTotalArticles != 0 {
78 rt.MaxTotalArticles = args.MaxTotalArticles
79 } else if cfg.MaxTotalArticles != 0 {
80 rt.MaxTotalArticles = cfg.MaxTotalArticles
81 }
82
83 // Content filtering - CLI args override config
84 if args.IncludeKeywords != "" {
85 rt.IncludeKeywords = cli.ParseKeywords(args.IncludeKeywords)
86 } else if cfg.IncludeKeywords != "" {
87 rt.IncludeKeywords = cli.ParseKeywords(cfg.IncludeKeywords)
88 }
89
90 if args.ExcludeKeywords != "" {
91 rt.ExcludeKeywords = cli.ParseKeywords(args.ExcludeKeywords)
92 } else if cfg.ExcludeKeywords != "" {
93 rt.ExcludeKeywords = cli.ParseKeywords(cfg.ExcludeKeywords)
94 }
95
96 // LLM API options - CLI args override config
97 if args.APIKey != "" {
98 rt.APIKey = args.APIKey
99 } else if cfg.APIKey != "" {
100 rt.APIKey = cfg.APIKey
101 } else {
102 // Fall back to environment variable
103 rt.APIKey = os.Getenv("LLM_AGGREGATOR_API_KEY")
104 }
105
106 if args.Model != "" {
107 rt.Model = args.Model
108 } else if cfg.Model != "" {
109 rt.Model = cfg.Model
110 }
111
112 if args.MaxTokens != 0 {
113 rt.MaxTokens = args.MaxTokens
114 } else if cfg.MaxTokens != 0 {
115 rt.MaxTokens = cfg.MaxTokens
116 }
117
118 if args.Temperature != 0 {
119 rt.Temperature = args.Temperature
120 } else if cfg.Temperature != 0 {
121 rt.Temperature = cfg.Temperature
122 }
123
124 // System prompt - CLI args override config
125 if args.SystemPrompt != "" {
126 rt.SystemPrompt = args.SystemPrompt
127 } else if cfg.SystemPrompt != "" {
128 rt.SystemPrompt = cfg.SystemPrompt
129 }
130
131 // Output options - CLI args override config
132 if args.Output != "" {
133 rt.Output = args.Output
134 } else if cfg.Output != "" {
135 rt.Output = cfg.Output
136 }
137
138 if args.OutputFile != "" {
139 rt.OutputFile = args.OutputFile
140 } else if cfg.OutputFile != "" {
141 rt.OutputFile = cfg.OutputFile
142 }
143
144 rt.IncludeArticles = args.IncludeArticles || cfg.IncludeArticles
145 rt.Verbose = args.Verbose
146}
147
148func runWithTUI(rt *runtime.Runtime) {
149 // 1. Create the model and pass the runtime to it.
150 model := tui.New(rt)
151
152 // 2. Create the tea.Program.
153 p := tea.NewProgram(model, tea.WithAltScreen())
154
155 // 3. Create the TUIProgress handler and give it the program instance.
156 // This is the bridge that allows the runtime to send messages to the TUI.
157 tp := tui.NewTUIProgress(p)
158
159 // 4. Inject the progress handler into the runtime.
160 rt.Progress = tp
161
162 // 5. Run the program. This is a blocking call.
163 if _, err := p.Run(); err != nil {
164 fmt.Fprintf(os.Stderr, "TUI Error: %v\n", err)
165 os.Exit(1)
166 }
167}
168
169func runWithoutTUI(rt *runtime.Runtime, verbose bool) {
170 // Setup logger based on verbose flag and inject it into the runtime
171 if verbose {
172 rt.Progress = progress.NewSimpleLogger(os.Stdout, true)
173 } else {
174 // Default is already NoopLogger, but we can be explicit
175 rt.Progress = &progress.NoopLogger{}
176 }
177
178 // Execute the runtime
179 err := rt.Execute(context.Background())
180 if err != nil {
181 fmt.Fprintf(os.Stderr, "Error: %v\n", err)
182 os.Exit(1)
183 }
184
185 // Write output
186 if rt.OutputFile != "" {
187 if err := rt.WriteOutputToFile(); err != nil {
188 fmt.Fprintf(os.Stderr, "Error writing output: %v\n", err)
189 os.Exit(1)
190 }
191 } else {
192 if err := rt.WriteOutput(os.Stdout); err != nil {
193 fmt.Fprintf(os.Stderr, "Error writing output: %v\n", err)
194 os.Exit(1)
195 }
196 }
197}