all repos — llm_aggregator @ 7208dd81944bf3bb82070208a69ea5d1d083dd01

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
  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	// Deepseek 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}