all repos — llm_aggregator @ f8680f68d04b67101a791bdcffbbb96f0bc740a8

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

internal/cli/help.go (view raw)

  1package cli
  2
  3import (
  4	"fmt"
  5	"os"
  6	"strings"
  7
  8	"github.com/charmbracelet/lipgloss"
  9	"codeberg.org/maxwelljensen/llm_aggregator/internal/style"
 10)
 11
 12type HelpOption struct {
 13	Short       string
 14	Name        string
 15	Description string
 16	Default     string
 17	Type        string
 18	Required    bool
 19}
 20
 21type HelpSection struct {
 22	Title   string
 23	Options []HelpOption
 24}
 25
 26var (
 27	headerStyle = lipgloss.NewStyle().Bold(true)
 28	flagStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color(style.Cyan))
 29)
 30
 31func shouldStyle() bool {
 32	return !style.NoColor()
 33}
 34
 35func renderFlag(short, long string) string {
 36	if short != "" {
 37		return fmt.Sprintf("%s, %s", short, long)
 38	}
 39	return long
 40}
 41
 42func getOptionValue(opt HelpOption) string {
 43	if opt.Type != "" {
 44		val := opt.Type
 45		if opt.Default != "" {
 46			val = fmt.Sprintf("%s (default: %s)", opt.Type, opt.Default)
 47		}
 48		return val
 49	}
 50	if opt.Default != "" {
 51		return opt.Default
 52	}
 53	return ""
 54}
 55
 56func maxFlagWidth(sections []HelpSection) int {
 57	maxWidth := 0
 58	for _, section := range sections {
 59		for _, opt := range section.Options {
 60			flagStr := renderFlag(opt.Short, opt.Name)
 61			width := lipgloss.Width(flagStr)
 62			maxWidth = max(maxWidth, width)
 63		}
 64	}
 65	return maxWidth
 66}
 67
 68func formatSection(section HelpSection, flagWidth int, _ int) string {
 69	var b strings.Builder
 70
 71	b.WriteString("\n")
 72	b.WriteString(headerStyle.Render(section.Title))
 73	b.WriteString("\n")
 74
 75	for _, opt := range section.Options {
 76		flagStr := renderFlag(opt.Short, opt.Name)
 77		padding := max(flagWidth-lipgloss.Width(flagStr), 0)
 78
 79		if shouldStyle() {
 80			b.WriteString(flagStyle.Render(flagStr))
 81		} else {
 82			b.WriteString(flagStr)
 83		}
 84		b.WriteString(strings.Repeat(" ", padding+2))
 85
 86		value := getOptionValue(opt)
 87		if value != "" {
 88			if shouldStyle() {
 89				b.WriteString(flagStyle.Render(value))
 90			} else {
 91				b.WriteString(value)
 92			}
 93			b.WriteString("  ")
 94		}
 95
 96		if opt.Required {
 97			b.WriteString("[required] ")
 98		}
 99
100		b.WriteString(opt.Description)
101		b.WriteString("\n")
102	}
103
104	return b.String()
105}
106
107func BuildStyledHelp(args *Args) string {
108	var b strings.Builder
109
110	b.WriteString(headerStyle.Render("llm_aggregator"))
111	b.WriteString("\n\n")
112
113	b.WriteString(args.Description())
114	b.WriteString("\n\n")
115
116	b.WriteString(headerStyle.Render("FLAGS"))
117	b.WriteString("\n")
118
119	sections := []HelpSection{
120		{
121			Title: "Required Arguments",
122			Options: []HelpOption{
123				{
124					Short:       "-p",
125					Name:        "--prompt PROMPT",
126					Description: "User prompt for summarisation/analysis",
127					Required:    true,
128				},
129			},
130		},
131		{
132			Title: "Feed Input",
133			Options: []HelpOption{
134				{
135					Short:       "-f",
136					Name:        "--feeds-file FILE",
137					Description: "Path to file containing RSS feed URLs (one per line)",
138				},
139				{
140					Name:        "--stdin",
141					Description: "Read a single RSS/Atom feed from stdin (can be combined with --feeds-file)",
142				},
143			},
144		},
145		{
146			Title: "Feed Aggregation",
147			Options: []HelpOption{
148				{
149					Short:       "-n",
150					Name:        "--max-articles-per-feed N",
151					Description: "Maximum articles to fetch from each feed",
152					Default:     "10",
153				},
154				{
155					Short:       "-d",
156					Name:        "--max-days-old N",
157					Description: "Only include articles from the last N days (0 for all)",
158					Default:     "7",
159				},
160				{
161					Name:        "--max-total-articles N",
162					Description: "Maximum total articles to process across all feeds",
163					Default:     "20",
164				},
165			},
166		},
167		{
168			Title: "Content Filtering",
169			Options: []HelpOption{
170				{
171					Short:       "-i",
172					Name:        "--include-keywords LIST",
173					Description: "Comma-separated keywords to include (case-insensitive)",
174				},
175				{
176					Short:       "-e",
177					Name:        "--exclude-keywords LIST",
178					Description: "Comma-separated keywords to exclude (case-insensitive)",
179				},
180			},
181		},
182		{
183			Title: "LLM API Options",
184			Options: []HelpOption{
185				{
186					Name:        "--api-key KEY",
187					Description: "OpenAI-compatible API key (default: read from $LLM_AGGREGATOR_API_KEY)",
188				},
189				{
190					Name:        "--base-url URL",
191					Description: "API base URL",
192					Default:     "https://api.deepseek.com",
193				},
194				{
195					Short:       "-m",
196					Name:        "--model MODEL",
197					Description: "LLM model to use",
198					Default:     "deepseek-chat",
199				},
200				{
201					Name:        "--max-tokens N",
202					Description: "Maximum tokens in LLM response",
203					Default:     "4000",
204				},
205				{
206					Name:        "--temperature VALUE",
207					Description: "Sampling temperature (0.0 to 1.0)",
208					Default:     "0.7",
209				},
210				{
211					Name:        "--timeout N",
212					Description: "LLM request timeout in seconds (default: 300)",
213					Default:     "300",
214				},
215				{
216					Name:        "--system-prompt TEXT",
217					Description: "Custom system prompt for LLM",
218				},
219			},
220		},
221		{
222			Title: "Output Options",
223			Options: []HelpOption{
224				{
225					Short:       "-o",
226					Name:        "--output FORMAT",
227					Description: "Output format",
228					Type:        "text|markdown|json",
229					Default:     "text",
230				},
231				{
232					Name:        "--output-file FILE",
233					Description: "Write output to FILE instead of stdout",
234				},
235				{
236					Name:        "--include-articles",
237					Description: "Include original articles in JSON output",
238				},
239				{
240					Short:       "-P",
241					Name:        "--plain",
242					Description: "Output only the raw LLM response without any formatting or metadata",
243				},
244			},
245		},
246		{
247			Title: "Interface Options",
248			Options: []HelpOption{
249				{
250					Short:       "-t",
251					Name:        "--tui",
252					Description: "Enable TUI interface with progress bar, live counters, and elapsed time",
253				},
254				{
255					Short:       "-D",
256					Name:        "--dry-run",
257					Description: "Validate config, show article statistics, and exit without making LLM API calls",
258				},
259				{
260					Short:       "-v",
261					Name:        "--verbose",
262					Description: "Enable verbose logging",
263				},
264			},
265		},
266		{
267			Title: "General Options",
268			Options: []HelpOption{
269				{
270					Name:        "--version",
271					Description: "Show version information and exit",
272				},
273				{
274					Short:       "-h",
275					Name:        "--help",
276					Description: "Show this help message and exit",
277				},
278			},
279		},
280	}
281
282	flagWidth := maxFlagWidth(sections)
283
284	for _, section := range sections {
285		b.WriteString(formatSection(section, flagWidth, 0))
286	}
287
288	b.WriteString("\n")
289	b.WriteString(headerStyle.Render("Examples:"))
290	b.WriteString("\n\n")
291
292	examples := []struct {
293		cmd  string
294		desc string
295	}{
296		{"llm_aggregator -f feeds.txt -p \"Summarise news\"", "Basic usage with feeds file"},
297		{"curl -s https://example.com/feed.xml | llm_aggregator --stdin -p \"Summarise\"", "RSS from stdin"},
298		{"llm_aggregator -f feeds.txt --stdin -p \"Summarise all\"", "Combine feeds file with stdin feed"},
299		{"llm_aggregator -f feeds.txt -p \"Linux news\" -i linux,opensource -d 3", "Filter by keywords and age"},
300		{"llm_aggregator -f feeds.txt -p \"Summarise tech news\" -t", "With TUI progress bar"},
301		{"llm_aggregator -f feeds.txt -p \"Summarise news\" -D", "Dry run (no LLM API calls)"},
302		{"llm_aggregator -f feeds.txt -p \"Analyse AI\" -o json --output-file output.json", "JSON output to file"},
303		{"llm_aggregator -f feeds.txt -p \"Summarise news\" -P", "Plain output (no metadata)"},
304	}
305
306	for _, ex := range examples {
307		if shouldStyle() {
308			fmt.Fprintf(&b, "  %s\n    %s\n\n", flagStyle.Render(ex.cmd), ex.desc)
309		} else {
310			fmt.Fprintf(&b, "  %s\n    %s\n\n", ex.cmd, ex.desc)
311		}
312	}
313
314	return b.String()
315}
316
317func WriteHelp(args *Args, w *os.File) {
318	output := BuildStyledHelp(args)
319	w.WriteString(output) //nolint:errcheck
320}