diff --git a/README.md b/README.md index 8ceea5f..7d97c81 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ keenable search "query" -p # Pretty output (for keenable search "AI news" --site techcrunch.com # Restrict to site keenable search "query" --published-after 2026-01-01 # Date filter keenable search "query" --acquired-before 2026-05-01 # Date filter +keenable search "query" --snippet-max-length 2000 # Longer snippets (180-10000) keenable search "query" --api-key KEY # Use a specific API key ``` diff --git a/src/commands/search.rs b/src/commands/search.rs index f6fa695..bc04f82 100644 --- a/src/commands/search.rs +++ b/src/commands/search.rs @@ -285,6 +285,7 @@ pub async fn search( query: &str, mode: Option<&str>, filters: SearchFilters, + snippet_max_length: Option, human: bool, api_key: Option<&str>, ) { @@ -315,6 +316,9 @@ pub async fn search( if let Some(m) = &effective_mode { body["mode"] = json!(m); } + if let Some(n) = snippet_max_length { + body["snippet_max_length"] = json!(n); + } // Merge filter fields into body if let Value::Object(filter_map) = filters.to_json() && let Value::Object(ref mut body_map) = body diff --git a/src/main.rs b/src/main.rs index 13d7ca5..9983728 100644 --- a/src/main.rs +++ b/src/main.rs @@ -137,7 +137,7 @@ enum Commands { /// Search the web (outputs YAML by default, use -p for pretty output) #[command( - after_help = "Works without login (free tier). Log in for higher rate limits.\n\nExamples:\n keenable search \"rust async\" YAML output (for agents)\n keenable search \"rust async\" -p Pretty output (for humans)\n keenable search \"AI news\" --site techcrunch.com Restrict to site\n keenable search \"dodgers braves\" --published-after 2026-01-01 Date filter (YYYY-MM-DD)\n keenable search \"AI news\" --acquired-after 7d Relative date (min, h, d, mo, y)\n keenable search \"AI news\" --acquired-after 2026-01-15T10:30:00Z ISO 8601 datetime\n keenable search \"rust async\" --api-key keen_***_***** Use a specific API key" + after_help = "Works without login (free tier). Log in for higher rate limits.\n\nExamples:\n keenable search \"rust async\" YAML output (for agents)\n keenable search \"rust async\" -p Pretty output (for humans)\n keenable search \"AI news\" --site techcrunch.com Restrict to site\n keenable search \"dodgers braves\" --published-after 2026-01-01 Date filter (YYYY-MM-DD)\n keenable search \"AI news\" --acquired-after 7d Relative date (min, h, d, mo, y)\n keenable search \"AI news\" --acquired-after 2026-01-15T10:30:00Z ISO 8601 datetime\n keenable search \"rust async\" --snippet-max-length 2000 Longer snippets (180-10000)\n keenable search \"rust async\" --api-key keen_***_***** Use a specific API key" )] Search { /// Search query @@ -167,6 +167,10 @@ enum Commands { #[arg(long)] published_before: Option, + /// Maximum snippet length in characters (API accepts 180-10000) + #[arg(long = "snippet-max-length")] + snippet_max_length: Option, + /// Pretty-print output for humans instead of YAML #[arg(short = 'p', long = "pretty")] pretty: bool, @@ -317,6 +321,7 @@ async fn main() { acquired_before, published_after, published_before, + snippet_max_length, pretty, api_key, } => { @@ -327,8 +332,15 @@ async fn main() { published_after, published_before, }; - commands::search::search(&query, mode.as_deref(), filters, pretty, api_key.as_deref()) - .await; + commands::search::search( + &query, + mode.as_deref(), + filters, + snippet_max_length, + pretty, + api_key.as_deref(), + ) + .await; } Commands::Fetch { url, diff --git a/tests/e2e/test_search.py b/tests/e2e/test_search.py index cf7a178..b6ca2bb 100644 --- a/tests/e2e/test_search.py +++ b/tests/e2e/test_search.py @@ -47,6 +47,28 @@ def test_result_count(basic_search): print(f"\nResult count for 'rust async patterns': {count}") +def test_snippet_max_length(kn): + def avg_snippet(data): + lens = [len(r.get("snippet") or "") for r in results_of(data)] + return sum(lens) / len(lens) + + short = kn("search", SEARCH_QUERY, "--snippet-max-length", "180").yaml() + long = kn("search", SEARCH_QUERY, "--snippet-max-length", "5000").yaml() + # Caps overshoot by ~10% (applied per-fragment upstream), so assert loose + # bounds far apart instead of exact limits. A dropped param would give + # both runs the same default length and fail one of the two. + assert avg_snippet(short) < 500 + assert avg_snippet(long) > 1000 + + +def test_snippet_max_length_out_of_range(kn): + res = kn("search", SEARCH_QUERY, "--snippet-max-length", "50") + assert res.code == 1 + data = res.yaml() + assert data["error"] == "Invalid parameter" + assert "snippet_max_length" in data["message"] + + # --- 2.2 modes --- def test_mode_realtime(kn):