internal/cli/args.go (view raw)
1package cli
2
3import (
4 "fmt"
5 "os"
6 "strings"
7
8 "github.com/alexflint/go-arg"
9)
10
11var (
12 Version string
13 BuildDate string
14)
15
16// Args represents command-line arguments.
17type Args struct {
18 FeedsFile string `arg:"--feeds-file,required" help:"Path to file containing RSS feed URLs (one per line)"`
19 Prompt string `arg:"--prompt,required" help:"User prompt for summarisation/analysis"`
20
21 // Feed aggregation options
22 MaxArticlesPerFeed *int `arg:"--max-articles-per-feed" help:"Maximum articles to fetch from each feed"`
23 MaxDaysOld *int `arg:"--max-days-old" help:"Only include articles from the last N days"`
24 MaxTotalArticles *int `arg:"--max-total-articles" help:"Maximum total articles to process"`
25
26 // Content filtering
27 IncludeKeywords string `arg:"--include-keywords" help:"Comma-separated list of keywords to include (case-insensitive)"`
28 ExcludeKeywords string `arg:"--exclude-keywords" help:"Comma-separated list of keywords to exclude (case-insensitive)"`
29
30 // LLM API options
31 APIKey *string `arg:"--api-key" help:"OpenAI-compatible API key (default: read from LLM_AGGREGATOR_API_KEY env var)"`
32 BaseURL *string `arg:"--base-url" help:"API base URL"`
33 Model *string `arg:"--model" help:"LLM model to use"`
34 MaxTokens *int `arg:"--max-tokens" help:"Maximum tokens in response"`
35 Temperature *float64 `arg:"--temperature" help:"Sampling temperature (0.0 to 1.0)"`
36
37 // Output options
38 Output string `arg:"--output" help:"Output format" choice:"text,json,markdown"`
39 OutputFile string `arg:"--output-file" help:"Write output to file (default: stdout)"`
40 IncludeArticles bool `arg:"--include-articles" help:"Include original articles in JSON output"`
41
42 // System options
43 SystemPrompt string `arg:"--system-prompt" help:"Custom system prompt for LLM"`
44 TUI bool `arg:"--tui" help:"Enable TUI interface with progress bar"`
45 Verbose bool `arg:"-v,--verbose" help:"Show verbose output"`
46 ShowVersion bool `arg:"--version" help:"Show version"`
47 DryRun bool `arg:"--dry-run" help:"Validate config, show article statistics, and exit without making LLM API calls"`
48}
49
50// Version returns the version string.
51func (Args) Version() string {
52 return fmt.Sprintf("llm_aggregator v%s (built %s)", Version, BuildDate)
53}
54
55// Description returns the program description.
56func (Args) Description() string {
57 return `LLM Aggregator - Aggregate RSS feeds and summarise with LLM API
58
59Examples:
60 # Basic usage with prompts
61 llm_aggregator --feeds-file feeds.txt --prompt "What are the latest trends in free software?"
62
63 # Validate configuration and preview articles (no LLM API calls)
64 llm_aggregator --feeds-file feeds.txt --prompt "Summarise news" --dry-run
65
66 # With custom LLM model
67 llm_aggregator --feeds-file feeds.txt --prompt "Summarise tech news" --model deepseek-coder
68
69 # Output to JSON file
70 llm_aggregator --feeds-file feeds.txt --prompt "Analyse AI developments" --output json --output-file analysis.json
71
72 # Filter by keywords
73 llm_aggregator --feeds-file feeds.txt --prompt "Linux news" --include-keywords linux,opensource
74
75Environment Variables:
76 LLM_AGGREGATOR_API_KEY: Your LLM API key (required if not provided via --api-key)`
77}
78
79// ParseKeywords parses comma-separated keywords string into list.
80func ParseKeywords(keywordString string) []string {
81 if keywordString == "" {
82 return nil
83 }
84 keywords := strings.Split(keywordString, ",")
85 result := make([]string, 0, len(keywords))
86 for _, kw := range keywords {
87 if trimmed := strings.TrimSpace(kw); trimmed != "" {
88 result = append(result, trimmed)
89 }
90 }
91 return result
92}
93
94// ParseArgs parses command line arguments.
95func ParseArgs() (*Args, error) {
96 var args Args
97 parser, err := arg.NewParser(arg.Config{
98 Program: "llm_aggregator",
99 }, &args)
100 if err != nil {
101 return nil, err
102 }
103
104 // Handle help and version flags before checking required fields
105 if len(os.Args) > 1 {
106 if os.Args[1] == "-h" || os.Args[1] == "--help" {
107 parser.WriteHelp(os.Stdout)
108 os.Exit(0)
109 }
110 if os.Args[1] == "--version" {
111 fmt.Printf("llm_aggregator v%s (built %s)", Version, BuildDate)
112 os.Exit(0)
113 }
114 }
115
116 err = parser.Parse(os.Args[1:])
117 if err != nil {
118 return nil, err
119 }
120
121 return &args, nil
122}
123
124// ToViperMap converts Args to a map for binding to Viper.
125// Only non-nil values (explicitly provided CLI flags) are included.
126func (a *Args) ToViperMap() map[string]any {
127 m := map[string]any{}
128 if a.FeedsFile != "" {
129 m["feeds_file"] = a.FeedsFile
130 }
131 if a.MaxArticlesPerFeed != nil {
132 m["max_articles_per_feed"] = *a.MaxArticlesPerFeed
133 }
134 if a.MaxDaysOld != nil {
135 m["max_days_old"] = *a.MaxDaysOld
136 }
137 if a.MaxTotalArticles != nil {
138 m["max_total_articles"] = *a.MaxTotalArticles
139 }
140 if a.IncludeKeywords != "" {
141 m["include_keywords"] = a.IncludeKeywords
142 }
143 if a.ExcludeKeywords != "" {
144 m["exclude_keywords"] = a.ExcludeKeywords
145 }
146 if a.APIKey != nil {
147 m["api_key"] = *a.APIKey
148 }
149 if a.BaseURL != nil {
150 m["base_url"] = *a.BaseURL
151 }
152 if a.Model != nil {
153 m["model"] = *a.Model
154 }
155 if a.MaxTokens != nil {
156 m["max_tokens"] = *a.MaxTokens
157 }
158 if a.Temperature != nil {
159 m["temperature"] = *a.Temperature
160 }
161 if a.Prompt != "" {
162 m["prompt"] = a.Prompt
163 }
164 if a.SystemPrompt != "" {
165 m["system_prompt"] = a.SystemPrompt
166 }
167 if a.Output != "" {
168 m["output"] = a.Output
169 }
170 if a.OutputFile != "" {
171 m["output_file"] = a.OutputFile
172 }
173 if a.IncludeArticles {
174 m["include_articles"] = a.IncludeArticles
175 }
176 return m
177}