feat: add --plain flag for raw LLM output without formatting
- Add `-P`/`--plain` CLI flag (`internal/cli/args.go`)
- Flag suppresses all formatting and metadata, outputting only the LLM
response
- Implement plain output logic in `internal/runtime/runtime.go`
- Skip formatter when plain mode is active, write summary directly
- Wire `--plain` through configuration layer
(`internal/config/config.go`)
- Map flag to runtime config, bind to `LLM_AGGREGATOR_PLAIN` env var
- Update man page (`docs/llm_aggregator.1`) with new option and
environment variable
- Add `--plain` to styled help and usage examples
(`internal/cli/help.go`)
- Extend tests for args-to-viper mapping and config parsing to cover
plain flag
- Bump version to 0.12.0 and modify `CHANGELOG.md` accordingly
- Bump man page version to 0.12.0 (drive-by)
@@ -6,6 +6,13 @@ 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.12.0] - 2026-04-26 + +### Added + +- **`-P/--plain` flag**: Output the raw LLM summary without any formatting or + metadata. Useful when the summary is to be consumed by downstream scripts. + ## [0.11.0] - 2026-04-23 - Progress bar now tracks estimated token count (via tiktoken) before sending
@@ -36,6 +36,7 @@ --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) + -P, --plain Output only the raw LLM response without formatting or metadata -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@@ -189,6 +190,7 @@ | `LLM_AGGREGATOR_EXCLUDE_KEYWORDS` | Comma‑separated exclude keywords |
| `LLM_AGGREGATOR_OUTPUT` | Output format (default: "text") | | `LLM_AGGREGATOR_OUTPUT_FILE` | Output file path | | `LLM_AGGREGATOR_INCLUDE_ARTICLES` | Include articles in JSON output (true/false) | +| `LLM_AGGREGATOR_PLAIN` | Plain output without metadata (true/false) | The API key can be provided via `--api‑key`, `LLM_AGGREGATOR_API_KEY` environment variable, or in the configuration file.
@@ -1,5 +1,5 @@
.\" Man page for llm_aggregator -.TH LLM_AGGREGATOR 1 "23 April 2026" "0.9.0" "User Commands" +.TH LLM_AGGREGATOR 1 "26 April 2026" "0.12.0" "User Commands" .SH NAME llm_aggregator \- aggregate RSS feeds and summarise them with LLMs .SH SYNOPSIS@@ -114,6 +114,13 @@ .B \-\-system\-prompt \fITEXT\fR
Custom system prompt for the LLM. If not specified, a default prompt is used. .SS Output Options .TP +.B \-P\fR,\fB \-\-plain +Output only the raw LLM response without any formatting or metadata. When +active, +.B -o\fR/ +.B \-\-output +has no effect. Useful for piping or post-processing. +.TP .B \-o\fR,\fB -o \fIFORMAT\fR Output format. Must be one of: .RS 12@@ -135,6 +142,13 @@ .B \-\-include\-articles
Include the original articles in JSON output. Only applicable when .B -o json is used. +.TP +.B \-P\fR,\fB \-\-plain +Output only the raw LLM response without any formatting or metadata. When +active, +.B -o\fR/ +.B \-\-output +has no effect. Useful for piping or post-processing. .SS Interface Options .TP .B \-D\fR,\fB \-\-dry\-run@@ -215,6 +229,9 @@ Output file path
.TP .B LLM_AGGREGATOR_INCLUDE_ARTICLES Include articles in JSON output (true/false) +.TP +.B LLM_AGGREGATOR_PLAIN +Plain output without metadata (true/false) .SH CONFIGURATION FILE Configuration can also be provided via a TOML file at: .PP
@@ -38,6 +38,7 @@ // Output options
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"` + Plain bool `arg:"-P,--plain" help:"Output only the raw LLM response without any formatting or metadata"` // System options SystemPrompt string `arg:"--system-prompt" help:"Custom system prompt for LLM"`@@ -158,6 +159,9 @@ m["output_file"] = a.OutputFile
} if a.IncludeArticles { m["include_articles"] = a.IncludeArticles + } + if a.Plain { + m["plain"] = a.Plain } return m }
@@ -450,6 +450,7 @@ SystemPrompt: "Custom system prompt",
Output: "json", OutputFile: "/tmp/output.json", IncludeArticles: true, + Plain: true, } viperMap := args.ToViperMap()@@ -471,6 +472,7 @@ {"system_prompt", "Custom system prompt"},
{"output", "json"}, {"output_file", "/tmp/output.json"}, {"include_articles", true}, + {"plain", true}, } for _, tt := range tests {
@@ -223,6 +223,11 @@ {
Name: "--include-articles", Description: "Include original articles in JSON output", }, + { + Short: "-P", + Name: "--plain", + Description: "Output only the raw LLM response without any formatting or metadata", + }, }, }, {@@ -280,6 +285,7 @@ {"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"}, + {"llm_aggregator -f feeds.txt -p \"Summarise news\" -P", "Plain output (no metadata)"}, } for _, ex := range examples {
@@ -37,6 +37,7 @@ // Output options
Output string `mapstructure:"output"` OutputFile string `mapstructure:"output_file"` IncludeArticles bool `mapstructure:"include_articles"` + Plain bool `mapstructure:"plain"` } // GetViper returns the global viper instance with all configuration sources set up.@@ -197,6 +198,7 @@ // Output options
rt.Output = v.GetString("output") rt.OutputFile = v.GetString("output_file") rt.IncludeArticles = v.GetBool("include_articles") + rt.Plain = v.GetBool("plain") return rt }@@ -220,6 +222,7 @@ // Output options
v.BindEnv("output", "LLM_AGGREGATOR_OUTPUT") v.BindEnv("output_file", "LLM_AGGREGATOR_OUTPUT_FILE") v.BindEnv("include_articles", "LLM_AGGREGATOR_INCLUDE_ARTICLES") + v.BindEnv("plain", "LLM_AGGREGATOR_PLAIN") } // GetConfigPath returns the path to the config file.
@@ -383,6 +383,7 @@ "--exclude-keywords": "advertisement",
"--max-tokens": "1000", "--temperature": "0.3", "--output": "json", + "--plain": "", }, expectFeeds: feedsFile, expectModel: "deepseek-coder",@@ -399,7 +400,10 @@ key := k
if strings.HasPrefix(key, "--") { key = key[2:] } - args = append(args, "--"+key, v) + args = append(args, "--"+key) + if v != "" { + args = append(args, v) + } } // Save original os.Args@@ -433,6 +437,11 @@ if apiKey, ok := tt.cliArgs["--api-key"]; ok {
if parsedArgs.APIKey != nil && *parsedArgs.APIKey != apiKey { t.Errorf("APIKey = %q, want %q", *parsedArgs.APIKey, apiKey) } + } + + // Verify plain flag + if tt.name == "all CLI options" && !parsedArgs.Plain { + t.Error("Plain should be true when --plain is provided") } }) }
@@ -34,6 +34,7 @@ SystemPrompt string
Output string OutputFile string IncludeArticles bool + Plain bool Verbose bool // State@@ -135,6 +136,13 @@ }
// WriteOutput writes the formatted output to the specified writer func (r *Runtime) WriteOutput(writer io.Writer) error { + if r.Plain { + if _, err := fmt.Fprint(writer, r.Summary); err != nil { + return fmt.Errorf("error writing plain output: %w", err) + } + return nil + } + formatter, err := output.NewFormatter(r.Output) if err != nil { return fmt.Errorf("error creating formatter: %w", err)