feat: add short command-line flags and styled help output - Add single-character shortcuts for all commonly used options - `-f` / `--feeds-file` — feeds file path - `-p` / `--prompt` — summarisation prompt - `-n` / `--max-articles-per-feed` — articles per feed limit - `-d` / `--max-days-old` — article age filter - `-i` / `--include-keywords` — keyword include filter - `-e` / `--exclude-keywords` — keyword exclude filter - `-m` / `--model` — LLM model selection - `-o` / `--output` — output format - `-t` / `--tui` — enable TUI interface - `-D` / `--dry-run` — dry run mode - Replace go-arg's default plain-text help with custom lipgloss-styled output - Create `internal/cli/help.go` with sectioned, coloured help text - Flags rendered in cyan, headers in bold, consistent column alignment - Respect `$NO_COLOR` for terminal compatibility - Update `README.md` to show short flags in option tables and examples - Update `docs/llm_aggregator.1` man page with short flag documentation - Update `CHANGELOG.md` for version 0.10.0 - Update `.gitignore` to exclude `AGENTS.md` and `.crush`
@@ -6,6 +6,27 @@ The format is based on [Keep a
Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.10.0] - 2026-04-23 + +### Added + +- **Short command-line flags**: Single-character shortcuts for the most + commonly used options: + - `-f` / `--feeds-file` — feeds file path + - `-p` / `--prompt` — summarisation prompt + - `-n` / `--max-articles-per-feed` — articles per feed limit + - `-d` / `--max-days-old` — article age filter + - `-i` / `--include-keywords` — keyword include filter + - `-e` / `--exclude-keywords` — keyword exclude filter + - `-m` / `--model` — LLM model selection + - `-o` / `--output` — output format + - `-t` / `--tui` — enable TUI + - `-D` / `--dry-run` — dry run mode +- **Styled help output**: Custom lipgloss-styled help text at + `internal/cli/help.go` replaces go-arg's default plain-text output. Flags are + rendered in cyan, section headers in bold, with consistent column alignment. + Respects `$NO_COLOR`. + ## [0.9.0] - 2026-04-23 ### Added
@@ -30,28 +30,28 @@ (text, markdown, or JSON).
### Basic Options - --feeds-file FILE Path to file containing RSS feed URLs (one per line) - --prompt PROMPT User prompt for summarisation/analysis - --api-key KEY API key (default: read from $LLM_AGGREGATOR_API_KEY) - --model MODEL Model to use (default: deepseek-chat) - --dry-run Validate config, show article statistics, and exit without making LLM API calls. - --base-url URL API base URL (default: https://api.deepseek.com) - --max-tokens N Maximum tokens in response (default: 4000) - --output FORMAT Output format: text, markdown, or json (default: text) - --output-file FILE Write output to FILE instead of stdout - --tui Enable TUI interface with progress bar - --verbose, -v Enable verbose logging - --help, -h Show this help message and exit - --version Show version information and exit + -f, --feeds-file FILE Path to file containing RSS feed URLs (one per line) [required] + -p, --prompt PROMPT User prompt for summarisation/analysis [required] + --api-key KEY API key (default: read from $LLM_AGGREGATOR_API_KEY) + -m, --model MODEL Model to use (default: deepseek-chat) + --base-url URL API base URL (default: https://api.deepseek.com) + --max-tokens N Maximum tokens in response (default: 4000) + -o, --output FORMAT Output format: text, markdown, or json (default: text) + --output-file FILE Write output to FILE instead of STDOUT + -t, --tui Enable TUI interface with progress bar + -D, --dry-run Validate config, show article statistics, and exit without making LLM API calls + -v, --verbose Enable verbose logging + -h, --help Show this help message and exit + --version Show version information and exit ### Filtering & Processing - --max-articles-per-feed N Maximum articles to fetch from each feed (default: 10) - --max-days-old N Only include articles from the last N days (0 for all) (default: 7) - --max-total-articles N Maximum total articles to process (default: 20) - --include-keywords LIST Comma-separated list of keywords to include (case‑insensitive) - --exclude-keywords LIST Comma-separated list of keywords to exclude (case‑insensitive) - --include-articles Include original articles in JSON output + -n, --max-articles-per-feed N Maximum articles to fetch from each feed (default: 10) + -d, --max-days-old N Only include articles from the last N days (0 for all) (default: 7) + --max-total-articles N Maximum total articles to process (default: 20) + -i, --include-keywords LIST Comma-separated list of keywords to include (case‑insensitive) + -e, --exclude-keywords LIST Comma-separated list of keywords to exclude (case‑insensitive) + --include-articles Include original articles in JSON output ### LLM Configuration@@ -62,27 +62,27 @@ ### Examples
```bash # Basic usage: summarise tech news from a list of feeds -llm_aggregator --feeds-file feeds.txt \ ---prompt "What are the latest AI-related trends in free software?" +llm_aggregator -f feeds.txt -p "What are the latest AI-related trends in free software?" # With TUI progress bar -llm_aggregator --feeds-file feeds.txt --prompt "Summarise tech news" --tui +llm_aggregator -f feeds.txt -p "Summarise tech news" -t # Output to a JSON file with included articles -llm_aggregator --feeds-file feeds.txt --prompt "Analyse AI developments" \ - --output json --output-file analysis.json --include-articles +llm_aggregator -f feeds.txt -p "Analyse AI developments" \ + -o json --output-file analysis.json --include-articles + # Filter by keywords (only include articles about Linux or open source) -llm_aggregator --feeds-file feeds.txt --prompt "Linux news" \ - --include-keywords linux,opensource --max-days-old 3 +llm_aggregator -f feeds.txt -p "Linux news" \ + -i linux,opensource -d 3 # Use a custom model and higher token limit -llm_aggregator --feeds-file feeds.txt --prompt "Code analysis" \ - --model deepseek-reasoner --max-tokens 8000 +llm_aggregator -f feeds.txt -p "Code analysis" \ + -m deepseek-reasoner --max-tokens 8000 # Use a custom API endpoint (e.g., local Ollama) -llm_aggregator --feeds-file feeds.txt --prompt "Summarise news" \ - --base-url "http://localhost:11434/v1" --model llama3 +llm_aggregator -f feeds.txt -p "Summarise news" \ + --base-url "http://localhost:11434/v1" -m llama3 # Show version information llm_aggregator --version@@ -202,7 +202,7 @@ https://www.phoronix.com/rss.php
Then run: - llm_aggregator --feeds-file feeds.txt --prompt "Summarise the top tech stories" + llm_aggregator -f feeds.txt -p "Summarise the top tech stories" ## Dependencies
@@ -5,9 +5,9 @@ llm_aggregator \- aggregate RSS feeds and summarise them with LLMs
.SH SYNOPSIS .B llm_aggregator [\fIOPTIONS\fR] -.B \-\-feeds-file +.B \-f \fIFILE\fR -.B \-\-prompt +.B \-p \fIPROMPT\fR .SH DESCRIPTION .B llm_aggregator@@ -49,21 +49,21 @@ Feeds are fetched concurrently for optimal performance, with rate limiting
to avoid overwhelming feed servers. .SH REQUIRED ARGUMENTS .TP -.B \-\-feeds-file \fIFILE\fR +.B \-f\fR,\fB -f \fIFILE\fR Path to a file containing RSS feed URLs, one per line. .TP -.B \-\-prompt \fIPROMPT\fR +.B \-p\fR,\fB -p \fIPROMPT\fR User prompt for summarisation or analysis. This is sent to the LLM along with the article content. .SH OPTIONS .SS Feed Aggregation Options .TP -.B \-\-max\-articles\-per\-feed \fIN\fR +.B \-n\fR,\fB \-\-max\-articles\-per\-feed \fIN\fR Maximum number of articles to fetch from each feed. .br Default: 10 .TP -.B \-\-max\-days\-old \fIN\fR +.B \-d\fR,\fB \-\-max\-days\-old \fIN\fR Only include articles from the last N days. Set to 0 to include all articles regardless of age. .br@@ -75,11 +75,11 @@ .br
Default: 20 .SS Content Filtering .TP -.B \-\-include\-keywords \fILIST\fR +.B \-i\fR,\fB \-\-include\-keywords \fILIST\fR Comma\-separated list of keywords to include. Only articles containing at least one of these keywords (case\-insensitive) will be processed. .TP -.B \-\-exclude\-keywords \fILIST\fR +.B \-e\fR,\fB \-\-exclude\-keywords \fILIST\fR Comma\-separated list of keywords to exclude. Any article containing one of these keywords (case\-insensitive) will be filtered out. .SS LLM API Options@@ -94,7 +94,7 @@ API base URL for the LLM service.
.br Default: https://api.deepseek.com .TP -.B \-\-model \fIMODEL\fR +.B \-m\fR,\fB -m \fIMODEL\fR LLM model to use for summarisation. .br Default: deepseek-chat@@ -114,7 +114,7 @@ .B \-\-system\-prompt \fITEXT\fR
Custom system prompt for the LLM. If not specified, a default prompt is used. .SS Output Options .TP -.B \-\-output \fIFORMAT\fR +.B \-o\fR,\fB -o \fIFORMAT\fR Output format. Must be one of: .RS 12 .TP 4@@ -128,16 +128,16 @@ .B json
JSON output with structured data .RE .TP -.B \-\-output\-file \fIFILE\fR +.B -o\-file \fIFILE\fR Write output to FILE instead of stdout. .TP .B \-\-include\-articles Include the original articles in JSON output. Only applicable when -.B \-\-output json +.B -o json is used. .SS Interface Options .TP -.B \-\-dry\-run +.B \-D\fR,\fB \-\-dry\-run Validate configuration and preview what would happen without making any LLM API calls. Useful for: .RS 4@@ -155,7 +155,7 @@ \(bu
Testing network connectivity to feeds .RE .TP -.B \-\-tui +.B \-t\fR,\fB -t Enable the TUI interface with a progress bar, live article counters, and elapsed time display. The TUI supports keyboard navigation (j/k, arrows, space, b, g/G) and mouse wheel scrolling.@@ -248,8 +248,8 @@ Summarise tech news from a list of feeds:
.PP .RS 4 .nf -llm_aggregator \-\-feeds-file feeds.txt \\ - \-\-prompt "What are the latest AI\-related trends in free software?" +llm_aggregator -f feeds.txt \\ + -p "What are the latest AI\-related trends in free software?" .fi .RE .SS Validating Configuration (Dry Run)@@ -258,7 +258,7 @@ Validate configuration and preview articles before making any LLM API calls:
.PP .RS 4 .nf -llm_aggregator \-\-feeds-file feeds.txt \-\-prompt "Summarise news" \-\-dry-run +llm_aggregator -f feeds.txt -p "Summarise news" -D .fi .RE .PP@@ -270,7 +270,7 @@ Enable the TUI interface with a progress bar:
.PP .RS 4 .nf -llm_aggregator \-\-feeds-file feeds.txt \-\-prompt "Summarise tech news" \-\-tui +llm_aggregator -f feeds.txt -p "Summarise tech news" -t .fi .RE .SS JSON Output with Articles@@ -279,8 +279,8 @@ Output to a JSON file with original articles included:
.PP .RS 4 .nf -llm_aggregator \-\-feeds-file feeds.txt \-\-prompt "Analyse AI developments" \\ - \-\-output json \-\-output-file analysis.json \-\-include\-articles +llm_aggregator -f feeds.txt -p "Analyse AI developments" \\ + -o json -o-file analysis.json \-\-include\-articles .fi .RE .SS Filtering by Keywords@@ -289,7 +289,7 @@ Only include articles about Linux or open source:
.PP .RS 4 .nf -llm_aggregator \-\-feeds-file feeds.txt \-\-prompt "Linux news" \\ +llm_aggregator -f feeds.txt -p "Linux news" \\ \-\-include\-keywords linux,opensource \-\-max\-days\-old 3 .fi .RE@@ -299,11 +299,11 @@ Use a custom model with a higher token limit or local endpoint:
.PP .RS 4 .nf -llm_aggregator \-\-feeds-file feeds.txt \-\-prompt "Code analysis" \\ - \-\-model deepseek-reasoner \-\-max-tokens 8000 +llm_aggregator -f feeds.txt -p "Code analysis" \\ + -m deepseek-reasoner \-\-max-tokens 8000 -llm_aggregator \-\-feeds-file feeds.txt \-\-prompt "Summarise news" \\ - \-\-base-url "http://localhost:11434/v1" \-\-model llama3 +llm_aggregator -f feeds.txt -p "Summarise news" \\ + \-\-base-url "http://localhost:11434/v1" -m llama3 .fi .RE .SS Custom System Prompt@@ -312,8 +312,8 @@ Provide a custom system prompt for different analysis styles:
.PP .RS 4 .nf -llm_aggregator \-\-feeds-file feeds.txt \\ - \-\-prompt "Identify security vulnerabilities" \\ +llm_aggregator -f feeds.txt \\ + -p "Identify security vulnerabilities" \\ \-\-system-prompt "You are a security expert. Focus on identifying \\ potential vulnerabilities, data breaches, and security advisories." .fi
@@ -15,36 +15,36 @@ )
// Args represents command-line arguments. type Args struct { - FeedsFile string `arg:"--feeds-file,required" help:"Path to file containing RSS feed URLs (one per line)"` - Prompt string `arg:"--prompt,required" help:"User prompt for summarisation/analysis"` + FeedsFile string `arg:"-f,--feeds-file,required" help:"Path to file containing RSS feed URLs (one per line)"` + Prompt string `arg:"-p,--prompt,required" help:"User prompt for summarisation/analysis"` // Feed aggregation options - MaxArticlesPerFeed *int `arg:"--max-articles-per-feed" help:"Maximum articles to fetch from each feed"` - MaxDaysOld *int `arg:"--max-days-old" help:"Only include articles from the last N days"` + MaxArticlesPerFeed *int `arg:"-n,--max-articles-per-feed" help:"Maximum articles to fetch from each feed"` + MaxDaysOld *int `arg:"-d,--max-days-old" help:"Only include articles from the last N days"` MaxTotalArticles *int `arg:"--max-total-articles" help:"Maximum total articles to process"` // Content filtering - IncludeKeywords string `arg:"--include-keywords" help:"Comma-separated list of keywords to include (case-insensitive)"` - ExcludeKeywords string `arg:"--exclude-keywords" help:"Comma-separated list of keywords to exclude (case-insensitive)"` + IncludeKeywords string `arg:"-i,--include-keywords" help:"Comma-separated list of keywords to include (case-insensitive)"` + ExcludeKeywords string `arg:"-e,--exclude-keywords" help:"Comma-separated list of keywords to exclude (case-insensitive)"` // LLM API options APIKey *string `arg:"--api-key" help:"OpenAI-compatible API key (default: read from LLM_AGGREGATOR_API_KEY env var)"` BaseURL *string `arg:"--base-url" help:"API base URL"` - Model *string `arg:"--model" help:"LLM model to use"` + Model *string `arg:"-m,--model" help:"LLM model to use"` MaxTokens *int `arg:"--max-tokens" help:"Maximum tokens in response"` Temperature *float64 `arg:"--temperature" help:"Sampling temperature (0.0 to 1.0)"` // Output options - Output string `arg:"--output" help:"Output format" choice:"text,json,markdown"` + Output string `arg:"-o,--output" help:"Output format" choice:"text,json,markdown"` OutputFile string `arg:"--output-file" help:"Write output to file (default: stdout)"` IncludeArticles bool `arg:"--include-articles" help:"Include original articles in JSON output"` // System options SystemPrompt string `arg:"--system-prompt" help:"Custom system prompt for LLM"` - TUI bool `arg:"--tui" help:"Enable TUI interface with progress bar"` + TUI bool `arg:"-t,--tui" help:"Enable TUI interface with progress bar"` Verbose bool `arg:"-v,--verbose" help:"Show verbose output"` ShowVersion bool `arg:"--version" help:"Show version"` - DryRun bool `arg:"--dry-run" help:"Validate config, show article statistics, and exit without making LLM API calls"` + DryRun bool `arg:"-D,--dry-run" help:"Validate config, show article statistics, and exit without making LLM API calls"` } // Version returns the version string.@@ -54,26 +54,7 @@ }
// Description returns the program description. func (Args) Description() string { - return `LLM Aggregator - Aggregate RSS feeds and summarise with LLM API - -Examples: - # Basic usage with prompts - llm_aggregator --feeds-file feeds.txt --prompt "What are the latest trends in free software?" - - # Validate configuration and preview articles (no LLM API calls) - llm_aggregator --feeds-file feeds.txt --prompt "Summarise news" --dry-run - - # With custom LLM model - llm_aggregator --feeds-file feeds.txt --prompt "Summarise tech news" --model deepseek-coder - - # Output to JSON file - llm_aggregator --feeds-file feeds.txt --prompt "Analyse AI developments" --output json --output-file analysis.json - - # Filter by keywords - llm_aggregator --feeds-file feeds.txt --prompt "Linux news" --include-keywords linux,opensource - -Environment Variables: - LLM_AGGREGATOR_API_KEY: Your LLM API key (required if not provided via --api-key)` + return "LLM Aggregator - Aggregate RSS feeds and summarise with LLM API" } // ParseKeywords parses comma-separated keywords string into list.@@ -104,7 +85,7 @@
// Handle help and version flags before checking required fields if len(os.Args) > 1 { if os.Args[1] == "-h" || os.Args[1] == "--help" { - parser.WriteHelp(os.Stdout) + WriteHelp(&args, os.Stdout) os.Exit(0) } if os.Args[1] == "--version" {
@@ -0,0 +1,349 @@
+package cli + +import ( + "fmt" + "os" + "strings" + + "github.com/charmbracelet/lipgloss" + "llm_aggregator/internal/style" +) + +type HelpOption struct { + Short string + Name string + Description string + Default string + Type string + Required bool +} + +type HelpSection struct { + Title string + Options []HelpOption +} + +var ( + headerStyle = lipgloss.NewStyle().Bold(true) + flagStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(style.Cyan)) +) + +func shouldStyle() bool { + return !style.NoColor() +} + +func renderFlag(short, long string) string { + if short != "" { + return fmt.Sprintf("%s, %s", short, long) + } + return fmt.Sprintf("%s", long) +} + +func getOptionValue(opt HelpOption) string { + if opt.Type != "" { + val := opt.Type + if opt.Default != "" { + val = fmt.Sprintf("%s (default: %s)", opt.Type, opt.Default) + } + return val + } + if opt.Default != "" { + return opt.Default + } + return "" +} + +func buildOptionLine(opt HelpOption, flagWidth int) string { + flagStr := renderFlag(opt.Short, opt.Name) + padding := max(flagWidth-lipgloss.Width(flagStr), 0) + + var line strings.Builder + + if shouldStyle() { + line.WriteString(flagStyle.Render(flagStr)) + line.WriteString(strings.Repeat(" ", padding+2)) + } else { + line.WriteString(flagStr) + line.WriteString(strings.Repeat(" ", padding+2)) + } + + value := getOptionValue(opt) + if value != "" { + if shouldStyle() { + line.WriteString(flagStyle.Render(value)) + line.WriteString(" ") + } else { + line.WriteString(value) + line.WriteString(" ") + } + } + + if opt.Required { + if shouldStyle() { + line.WriteString("[required] ") + } else { + line.WriteString("[required] ") + } + } + + line.WriteString(opt.Description) + + return line.String() +} + +func maxFlagWidth(sections []HelpSection) int { + maxWidth := 0 + for _, section := range sections { + for _, opt := range section.Options { + flagStr := renderFlag(opt.Short, opt.Name) + width := lipgloss.Width(flagStr) + maxWidth = max(maxWidth, width) + } + } + return maxWidth +} + +func formatSection(section HelpSection, flagWidth int, _ int) string { + var b strings.Builder + + b.WriteString("\n") + b.WriteString(headerStyle.Render(section.Title)) + b.WriteString("\n") + + for _, opt := range section.Options { + flagStr := renderFlag(opt.Short, opt.Name) + padding := max(flagWidth-lipgloss.Width(flagStr), 0) + + if shouldStyle() { + b.WriteString(flagStyle.Render(flagStr)) + } else { + b.WriteString(flagStr) + } + b.WriteString(strings.Repeat(" ", padding+2)) + + value := getOptionValue(opt) + if value != "" { + if shouldStyle() { + b.WriteString(flagStyle.Render(value)) + } else { + b.WriteString(value) + } + b.WriteString(" ") + } + + if opt.Required { + b.WriteString("[required] ") + } + + b.WriteString(opt.Description) + b.WriteString("\n") + } + + return b.String() +} + +var descriptionStyle = lipgloss.NewStyle() + +func getTerminalWidth() int { + if termWidth := os.Getenv("COLUMNS"); termWidth != "" { + var width int + if _, err := fmt.Sscanf(termWidth, "%d", &width); err == nil && width > 0 { + return width + } + } + return 80 +} + +func BuildStyledHelp(args *Args) string { + var b strings.Builder + + b.WriteString(headerStyle.Render("llm_aggregator")) + b.WriteString("\n\n") + + b.WriteString(args.Description()) + b.WriteString("\n\n") + + b.WriteString(headerStyle.Render("FLAGS")) + b.WriteString("\n") + + sections := []HelpSection{ + { + Title: "Required Arguments", + Options: []HelpOption{ + { + Short: "-f", + Name: "--feeds-file FILE", + Description: "Path to file containing RSS feed URLs (one per line)", + Required: true, + }, + { + Short: "-p", + Name: "--prompt PROMPT", + Description: "User prompt for summarisation/analysis", + Required: true, + }, + }, + }, + { + Title: "Feed Aggregation", + Options: []HelpOption{ + { + Short: "-n", + Name: "--max-articles-per-feed N", + Description: "Maximum articles to fetch from each feed", + Default: "10", + }, + { + Short: "-d", + Name: "--max-days-old N", + Description: "Only include articles from the last N days (0 for all)", + Default: "7", + }, + { + Name: "--max-total-articles N", + Description: "Maximum total articles to process across all feeds", + Default: "20", + }, + }, + }, + { + Title: "Content Filtering", + Options: []HelpOption{ + { + Short: "-i", + Name: "--include-keywords LIST", + Description: "Comma-separated keywords to include (case-insensitive)", + }, + { + Short: "-e", + Name: "--exclude-keywords LIST", + Description: "Comma-separated keywords to exclude (case-insensitive)", + }, + }, + }, + { + Title: "LLM API Options", + Options: []HelpOption{ + { + Name: "--api-key KEY", + Description: "OpenAI-compatible API key (default: read from $LLM_AGGREGATOR_API_KEY)", + }, + { + Name: "--base-url URL", + Description: "API base URL", + Default: "https://api.deepseek.com", + }, + { + Short: "-m", + Name: "--model MODEL", + Description: "LLM model to use", + Default: "deepseek-chat", + }, + { + Name: "--max-tokens N", + Description: "Maximum tokens in LLM response", + Default: "4000", + }, + { + Name: "--temperature VALUE", + Description: "Sampling temperature (0.0 to 1.0)", + Default: "0.7", + }, + { + Name: "--system-prompt TEXT", + Description: "Custom system prompt for LLM", + }, + }, + }, + { + Title: "Output Options", + Options: []HelpOption{ + { + Short: "-o", + Name: "--output FORMAT", + Description: "Output format", + Type: "text|markdown|json", + Default: "text", + }, + { + Name: "--output-file FILE", + Description: "Write output to FILE instead of stdout", + }, + { + Name: "--include-articles", + Description: "Include original articles in JSON output", + }, + }, + }, + { + Title: "Interface Options", + Options: []HelpOption{ + { + Short: "-t", + Name: "--tui", + Description: "Enable TUI interface with progress bar, live counters, and elapsed time", + }, + { + Short: "-D", + Name: "--dry-run", + Description: "Validate config, show article statistics, and exit without making LLM API calls", + }, + { + Short: "-v", + Name: "--verbose", + Description: "Enable verbose logging", + }, + }, + }, + { + Title: "General Options", + Options: []HelpOption{ + { + Name: "--version", + Description: "Show version information and exit", + }, + { + Short: "-h", + Name: "--help", + Description: "Show this help message and exit", + }, + }, + }, + } + + flagWidth := maxFlagWidth(sections) + + for _, section := range sections { + b.WriteString(formatSection(section, flagWidth, 0)) + } + + b.WriteString("\n") + b.WriteString(headerStyle.Render("Examples:")) + b.WriteString("\n\n") + + examples := []struct { + cmd string + desc string + }{ + {"llm_aggregator -f feeds.txt -p \"Summarise news\"", "Basic usage with short flags"}, + {"llm_aggregator -f feeds.txt -p \"Linux news\" -i linux,opensource -d 3", "Filter by keywords and age"}, + {"llm_aggregator -f feeds.txt -p \"Summarise tech news\" -t", "With TUI progress bar"}, + {"llm_aggregator -f feeds.txt -p \"Summarise news\" -D", "Dry run (no LLM API calls)"}, + {"llm_aggregator -f feeds.txt -p \"Analyse AI\" -o json --output-file output.json", "JSON output to file"}, + } + + for _, ex := range examples { + if shouldStyle() { + fmt.Fprintf(&b, " %s\n %s\n\n", flagStyle.Render(ex.cmd), ex.desc) + } else { + fmt.Fprintf(&b, " %s\n %s\n\n", ex.cmd, ex.desc) + } + } + + return b.String() +} + +func WriteHelp(args *Args, w *os.File) { + output := BuildStyledHelp(args) + w.WriteString(output) +}