all repos — llm_aggregator @ 6e3abd0665fe86906baf2db7652595d393f2ffc3

A CLI tool to aggregate RSS feeds and summarise them with LLMs

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}