all repos — llm_aggregator @ af6da634ea141bc028760fd78253d03cee78a82f

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

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:"-f,--feeds-file,required" help:"Path to file containing RSS feed URLs (one per line)"`
 19	Prompt    string `arg:"-p,--prompt,required" help:"User prompt for summarisation/analysis"`
 20
 21	// Feed aggregation options
 22	MaxArticlesPerFeed *int `arg:"-n,--max-articles-per-feed" help:"Maximum articles to fetch from each feed"`
 23	MaxDaysOld         *int `arg:"-d,--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:"-i,--include-keywords" help:"Comma-separated list of keywords to include (case-insensitive)"`
 28	ExcludeKeywords string `arg:"-e,--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:"-m,--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:"-o,--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	Plain           bool   `arg:"-P,--plain" help:"Output only the raw LLM response without any formatting or metadata"`
 42
 43	// System options
 44	SystemPrompt string `arg:"--system-prompt" help:"Custom system prompt for LLM"`
 45	TUI          bool   `arg:"-t,--tui" help:"Enable TUI interface with progress bar"`
 46	Verbose      bool   `arg:"-v,--verbose" help:"Show verbose output"`
 47	ShowVersion  bool   `arg:"--version" help:"Show version"`
 48	DryRun       bool   `arg:"-D,--dry-run" help:"Validate config, show article statistics, and exit without making LLM API calls"`
 49}
 50
 51// Version returns the version string.
 52func (Args) Version() string {
 53	return fmt.Sprintf("llm_aggregator v%s (built %s)", Version, BuildDate)
 54}
 55
 56// Description returns the program description.
 57func (Args) Description() string {
 58	return "LLM Aggregator - Aggregate RSS feeds and summarise with LLM API"
 59}
 60
 61// ParseKeywords parses comma-separated keywords string into list.
 62func ParseKeywords(keywordString string) []string {
 63	if keywordString == "" {
 64		return nil
 65	}
 66	keywords := strings.Split(keywordString, ",")
 67	result := make([]string, 0, len(keywords))
 68	for _, kw := range keywords {
 69		if trimmed := strings.TrimSpace(kw); trimmed != "" {
 70			result = append(result, trimmed)
 71		}
 72	}
 73	return result
 74}
 75
 76// ParseArgs parses command line arguments.
 77func ParseArgs() (*Args, error) {
 78	var args Args
 79	parser, err := arg.NewParser(arg.Config{
 80		Program: "llm_aggregator",
 81	}, &args)
 82	if err != nil {
 83		return nil, err
 84	}
 85
 86	// Handle help and version flags before checking required fields
 87	if len(os.Args) > 1 {
 88		if os.Args[1] == "-h" || os.Args[1] == "--help" {
 89			WriteHelp(&args, os.Stdout)
 90			os.Exit(0)
 91		}
 92		if os.Args[1] == "--version" {
 93			fmt.Printf("llm_aggregator v%s (built %s)", Version, BuildDate)
 94			os.Exit(0)
 95		}
 96	}
 97
 98	err = parser.Parse(os.Args[1:])
 99	if err != nil {
100		return nil, err
101	}
102
103	return &args, nil
104}
105
106// ToViperMap converts Args to a map for binding to Viper.
107// Only non-nil values (explicitly provided CLI flags) are included.
108//
109// NOTE: FeedsFile and Prompt are strings (not pointers), so they're ALWAYS
110// included if non-empty. This differs from optional fields which use pointer
111// types to detect explicit provision vs. default zero values. See isZero() in
112// config package.
113func (a *Args) ToViperMap() map[string]any {
114	m := map[string]any{}
115	if a.FeedsFile != "" {
116		m["feeds_file"] = a.FeedsFile
117	}
118	if a.MaxArticlesPerFeed != nil {
119		m["max_articles_per_feed"] = *a.MaxArticlesPerFeed
120	}
121	if a.MaxDaysOld != nil {
122		m["max_days_old"] = *a.MaxDaysOld
123	}
124	if a.MaxTotalArticles != nil {
125		m["max_total_articles"] = *a.MaxTotalArticles
126	}
127	if a.IncludeKeywords != "" {
128		m["include_keywords"] = a.IncludeKeywords
129	}
130	if a.ExcludeKeywords != "" {
131		m["exclude_keywords"] = a.ExcludeKeywords
132	}
133	if a.APIKey != nil {
134		m["api_key"] = *a.APIKey
135	}
136	if a.BaseURL != nil {
137		m["base_url"] = *a.BaseURL
138	}
139	if a.Model != nil {
140		m["model"] = *a.Model
141	}
142	if a.MaxTokens != nil {
143		m["max_tokens"] = *a.MaxTokens
144	}
145	if a.Temperature != nil {
146		m["temperature"] = *a.Temperature
147	}
148	if a.Prompt != "" {
149		m["prompt"] = a.Prompt
150	}
151	if a.SystemPrompt != "" {
152		m["system_prompt"] = a.SystemPrompt
153	}
154	if a.Output != "" {
155		m["output"] = a.Output
156	}
157	if a.OutputFile != "" {
158		m["output_file"] = a.OutputFile
159	}
160	if a.IncludeArticles {
161		m["include_articles"] = a.IncludeArticles
162	}
163	if a.Plain {
164		m["plain"] = a.Plain
165	}
166	return m
167}