cmd/llm_aggregator.go (view raw)
1package main
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "os/signal"
8 "syscall"
9
10 "llm_aggregator/internal/cli"
11 "llm_aggregator/internal/config"
12 "llm_aggregator/internal/runtime"
13 "llm_aggregator/internal/tui"
14
15 tea "github.com/charmbracelet/bubbletea"
16)
17
18var (
19 version string
20 buildDate string
21)
22
23func main() {
24 cli.BuildDate = buildDate
25 cli.Version = version
26
27 // Load configuration from file and environment variables
28 cfg, err := config.Load()
29 if err != nil {
30 fmt.Fprintf(os.Stderr, "Warning: could not load configuration: %v\n", err)
31 // Continue with defaults
32 cfg = config.DefaultConfig()
33 }
34
35 // Parse command line arguments
36 args, err := cli.ParseArgs()
37 if err != nil {
38 fmt.Fprintf(os.Stderr, "Error parsing arguments: %v\n", err)
39 os.Exit(1)
40 }
41
42 // Create runtime configuration
43 rt := runtime.NewRuntime()
44 rt.FeedsFile = args.FeedsFile
45 rt.Prompt = args.Prompt
46
47 // Apply configuration with precedence: CLI args > Environment vars > Config file > Defaults
48 applyConfiguration(rt, cfg, args)
49
50 // Validate API key
51 if rt.APIKey == "" {
52 fmt.Fprintln(os.Stderr, "Error: OpenAI-compatible API key is required. Set via --api-key, LLM_AGGREGATOR_API_KEY environment variable, or config file.")
53 os.Exit(1)
54 }
55
56 // Run with TUI if requested
57 if args.TUI {
58 runWithTUI(rt)
59 } else {
60 runWithoutTUI(rt, args.Verbose)
61 }
62}
63
64// applyConfiguration applies configuration with proper precedence
65func applyConfiguration(rt *runtime.Runtime, cfg *config.Config, args *cli.Args) {
66 // Feed aggregation options - CLI args override config
67 if args.MaxArticlesPerFeed != 0 {
68 rt.MaxArticlesPerFeed = args.MaxArticlesPerFeed
69 } else if cfg.MaxArticlesPerFeed != 0 {
70 rt.MaxArticlesPerFeed = cfg.MaxArticlesPerFeed
71 }
72
73 if args.MaxDaysOld != 0 {
74 rt.MaxDaysOld = args.MaxDaysOld
75 } else if cfg.MaxDaysOld != 0 {
76 rt.MaxDaysOld = cfg.MaxDaysOld
77 }
78
79 if args.MaxTotalArticles != 0 {
80 rt.MaxTotalArticles = args.MaxTotalArticles
81 } else if cfg.MaxTotalArticles != 0 {
82 rt.MaxTotalArticles = cfg.MaxTotalArticles
83 }
84
85 // Content filtering - CLI args override config
86 if args.IncludeKeywords != "" {
87 rt.IncludeKeywords = cli.ParseKeywords(args.IncludeKeywords)
88 } else if cfg.IncludeKeywords != "" {
89 rt.IncludeKeywords = cli.ParseKeywords(cfg.IncludeKeywords)
90 }
91
92 if args.ExcludeKeywords != "" {
93 rt.ExcludeKeywords = cli.ParseKeywords(args.ExcludeKeywords)
94 } else if cfg.ExcludeKeywords != "" {
95 rt.ExcludeKeywords = cli.ParseKeywords(cfg.ExcludeKeywords)
96 }
97
98 // Deepseek API options - CLI args override config
99 if args.APIKey != "" {
100 rt.APIKey = args.APIKey
101 } else if cfg.APIKey != "" {
102 rt.APIKey = cfg.APIKey
103 } else {
104 // Fall back to environment variable
105 rt.APIKey = os.Getenv("LLM_AGGREGATOR_API_KEY")
106 }
107
108 if args.Model != "" {
109 rt.Model = args.Model
110 } else if cfg.Model != "" {
111 rt.Model = cfg.Model
112 }
113
114 if args.MaxTokens != 0 {
115 rt.MaxTokens = args.MaxTokens
116 } else if cfg.MaxTokens != 0 {
117 rt.MaxTokens = cfg.MaxTokens
118 }
119
120 if args.Temperature != 0 {
121 rt.Temperature = args.Temperature
122 } else if cfg.Temperature != 0 {
123 rt.Temperature = cfg.Temperature
124 }
125
126 // System prompt - CLI args override config
127 if args.SystemPrompt != "" {
128 rt.SystemPrompt = args.SystemPrompt
129 } else if cfg.SystemPrompt != "" {
130 rt.SystemPrompt = cfg.SystemPrompt
131 }
132
133 // Output options - CLI args override config
134 if args.Output != "" {
135 rt.Output = args.Output
136 } else if cfg.Output != "" {
137 rt.Output = cfg.Output
138 }
139
140 if args.OutputFile != "" {
141 rt.OutputFile = args.OutputFile
142 } else if cfg.OutputFile != "" {
143 rt.OutputFile = cfg.OutputFile
144 }
145
146 rt.IncludeArticles = args.IncludeArticles || cfg.IncludeArticles
147 rt.Verbose = args.Verbose
148}
149
150func runWithTUI(rt *runtime.Runtime) {
151 // Create TUI model
152 m := tui.New()
153 p := tea.NewProgram(m, tea.WithAltScreen())
154
155 // Run TUI in goroutine
156 done := make(chan error)
157 go func() {
158 if _, err := p.Run(); err != nil {
159 done <- err
160 return
161 }
162 done <- nil
163 }()
164
165 // Run aggregation in another goroutine
166 go func() {
167 // Create context for cancellation
168 ctx, cancel := context.WithCancel(context.Background())
169 defer cancel()
170
171 // Set up signal handling
172 sigCh := make(chan os.Signal, 1)
173 signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
174
175 // Handle signals
176 go func() {
177 <-sigCh
178 p.Send(tui.Error("Operation cancelled"))
179 cancel()
180 }()
181
182 // Update TUI status
183 p.Send(tui.Step(1, fmt.Sprintf("Aggregating feeds from: %s", rt.FeedsFile)))
184
185 // Execute the runtime
186 err := rt.Execute(ctx)
187 if err != nil {
188 p.Send(tui.Error(err.Error()))
189 return
190 }
191
192 // Update TUI status
193 p.Send(tui.StepWithCounts(5, "Formatting output", len(rt.Articles), len(rt.Articles)))
194
195 // Write output
196 if rt.OutputFile != "" {
197 if err := rt.WriteOutputToFile(); err != nil {
198 p.Send(tui.Error(fmt.Sprintf("Error writing output: %v", err)))
199 return
200 }
201 p.Send(tui.Step(6, fmt.Sprintf("Output written to: %s", rt.OutputFile)))
202 } else {
203 if err := rt.WriteOutput(os.Stdout); err != nil {
204 p.Send(tui.Error(fmt.Sprintf("Error writing output: %v", err)))
205 return
206 }
207 p.Send(tui.Step(6, "Output complete"))
208 }
209
210 // Mark as done
211 p.Send(tui.Step(7, "Processing completed successfully"))
212 }()
213
214 // Wait for TUI to complete
215 if err := <-done; err != nil {
216 fmt.Fprintf(os.Stderr, "TUI error: %v\n", err)
217 os.Exit(1)
218 }
219}
220
221func runWithoutTUI(rt *runtime.Runtime, verbose bool) {
222 // Set verbose flag on runtime
223 rt.Verbose = verbose
224
225 // Execute the runtime
226 err := rt.Execute(context.Background())
227 if err != nil {
228 fmt.Fprintf(os.Stderr, "Error: %v\n", err)
229 os.Exit(1)
230 }
231
232 // Write output
233 if rt.OutputFile != "" {
234 if err := rt.WriteOutputToFile(); err != nil {
235 fmt.Fprintf(os.Stderr, "Error writing output: %v\n", err)
236 os.Exit(1)
237 }
238 } else {
239 if err := rt.WriteOutput(os.Stdout); err != nil {
240 fmt.Fprintf(os.Stderr, "Error writing output: %v\n", err)
241 os.Exit(1)
242 }
243 }
244}