all repos — llm_aggregator @ b4f6beb3ba126b91482bcd5df0745445088d51d7

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	"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 fmt.Sprintf("%s", 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:       "-f",
125					Name:        "--feeds-file FILE",
126					Description: "Path to file containing RSS feed URLs (one per line)",
127					Required:    true,
128				},
129				{
130					Short:       "-p",
131					Name:        "--prompt PROMPT",
132					Description: "User prompt for summarisation/analysis",
133					Required:    true,
134				},
135			},
136		},
137		{
138			Title: "Feed Aggregation",
139			Options: []HelpOption{
140				{
141					Short:       "-n",
142					Name:        "--max-articles-per-feed N",
143					Description: "Maximum articles to fetch from each feed",
144					Default:     "10",
145				},
146				{
147					Short:       "-d",
148					Name:        "--max-days-old N",
149					Description: "Only include articles from the last N days (0 for all)",
150					Default:     "7",
151				},
152				{
153					Name:        "--max-total-articles N",
154					Description: "Maximum total articles to process across all feeds",
155					Default:     "20",
156				},
157			},
158		},
159		{
160			Title: "Content Filtering",
161			Options: []HelpOption{
162				{
163					Short:       "-i",
164					Name:        "--include-keywords LIST",
165					Description: "Comma-separated keywords to include (case-insensitive)",
166				},
167				{
168					Short:       "-e",
169					Name:        "--exclude-keywords LIST",
170					Description: "Comma-separated keywords to exclude (case-insensitive)",
171				},
172			},
173		},
174		{
175			Title: "LLM API Options",
176			Options: []HelpOption{
177				{
178					Name:        "--api-key KEY",
179					Description: "OpenAI-compatible API key (default: read from $LLM_AGGREGATOR_API_KEY)",
180				},
181				{
182					Name:        "--base-url URL",
183					Description: "API base URL",
184					Default:     "https://api.deepseek.com",
185				},
186				{
187					Short:       "-m",
188					Name:        "--model MODEL",
189					Description: "LLM model to use",
190					Default:     "deepseek-chat",
191				},
192				{
193					Name:        "--max-tokens N",
194					Description: "Maximum tokens in LLM response",
195					Default:     "4000",
196				},
197				{
198					Name:        "--temperature VALUE",
199					Description: "Sampling temperature (0.0 to 1.0)",
200					Default:     "0.7",
201				},
202				{
203					Name:        "--system-prompt TEXT",
204					Description: "Custom system prompt for LLM",
205				},
206			},
207		},
208		{
209			Title: "Output Options",
210			Options: []HelpOption{
211				{
212					Short:       "-o",
213					Name:        "--output FORMAT",
214					Description: "Output format",
215					Type:        "text|markdown|json",
216					Default:     "text",
217				},
218				{
219					Name:        "--output-file FILE",
220					Description: "Write output to FILE instead of stdout",
221				},
222				{
223					Name:        "--include-articles",
224					Description: "Include original articles in JSON output",
225				},
226			},
227		},
228		{
229			Title: "Interface Options",
230			Options: []HelpOption{
231				{
232					Short:       "-t",
233					Name:        "--tui",
234					Description: "Enable TUI interface with progress bar, live counters, and elapsed time",
235				},
236				{
237					Short:       "-D",
238					Name:        "--dry-run",
239					Description: "Validate config, show article statistics, and exit without making LLM API calls",
240				},
241				{
242					Short:       "-v",
243					Name:        "--verbose",
244					Description: "Enable verbose logging",
245				},
246			},
247		},
248		{
249			Title: "General Options",
250			Options: []HelpOption{
251				{
252					Name:        "--version",
253					Description: "Show version information and exit",
254				},
255				{
256					Short:       "-h",
257					Name:        "--help",
258					Description: "Show this help message and exit",
259				},
260			},
261		},
262	}
263
264	flagWidth := maxFlagWidth(sections)
265
266	for _, section := range sections {
267		b.WriteString(formatSection(section, flagWidth, 0))
268	}
269
270	b.WriteString("\n")
271	b.WriteString(headerStyle.Render("Examples:"))
272	b.WriteString("\n\n")
273
274	examples := []struct {
275		cmd  string
276		desc string
277	}{
278		{"llm_aggregator -f feeds.txt -p \"Summarise news\"", "Basic usage with short flags"},
279		{"llm_aggregator -f feeds.txt -p \"Linux news\" -i linux,opensource -d 3", "Filter by keywords and age"},
280		{"llm_aggregator -f feeds.txt -p \"Summarise tech news\" -t", "With TUI progress bar"},
281		{"llm_aggregator -f feeds.txt -p \"Summarise news\" -D", "Dry run (no LLM API calls)"},
282		{"llm_aggregator -f feeds.txt -p \"Analyse AI\" -o json --output-file output.json", "JSON output to file"},
283	}
284
285	for _, ex := range examples {
286		if shouldStyle() {
287			fmt.Fprintf(&b, "  %s\n    %s\n\n", flagStyle.Render(ex.cmd), ex.desc)
288		} else {
289			fmt.Fprintf(&b, "  %s\n    %s\n\n", ex.cmd, ex.desc)
290		}
291	}
292
293	return b.String()
294}
295
296func WriteHelp(args *Args, w *os.File) {
297	output := BuildStyledHelp(args)
298	w.WriteString(output)
299}