From c5ddaf694acd9936c7d0652578bce4a42b60a59a Mon Sep 17 00:00:00 2001 From: Ilya Gusev Date: Sun, 16 Aug 2026 19:04:51 +0000 Subject: [PATCH 1/2] feat: add --query-time flag to search Point-in-time search: sends query_time in the search body, matching the REST API and the MCP tool schema (keenable-backend-ts#354). Co-Authored-By: Claude Fable 5 --- src/commands/search.rs | 4 ++++ src/main.rs | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/commands/search.rs b/src/commands/search.rs index bc04f82..0181074 100644 --- a/src/commands/search.rs +++ b/src/commands/search.rs @@ -286,6 +286,7 @@ pub async fn search( mode: Option<&str>, filters: SearchFilters, snippet_max_length: Option, + query_time: Option<&str>, human: bool, api_key: Option<&str>, ) { @@ -319,6 +320,9 @@ pub async fn search( if let Some(n) = snippet_max_length { body["snippet_max_length"] = json!(n); } + if let Some(qt) = query_time { + body["query_time"] = json!(qt); + } // 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 9983728..7496384 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\" --snippet-max-length 2000 Longer snippets (180-10000)\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 \"AI news\" --query-time 2026-01-01T00:00:00Z Point-in-time search\n keenable search \"rust async\" --api-key keen_***_***** Use a specific API key" )] Search { /// Search query @@ -171,6 +171,11 @@ enum Commands { #[arg(long = "snippet-max-length")] snippet_max_length: Option, + /// Point-in-time search: only pages available on or before this + /// timestamp (ISO 8601, e.g. 2024-01-01T00:00:00Z, or relative e.g. 7d) + #[arg(long = "query-time")] + query_time: Option, + /// Pretty-print output for humans instead of YAML #[arg(short = 'p', long = "pretty")] pretty: bool, @@ -322,6 +327,7 @@ async fn main() { published_after, published_before, snippet_max_length, + query_time, pretty, api_key, } => { @@ -337,6 +343,7 @@ async fn main() { mode.as_deref(), filters, snippet_max_length, + query_time.as_deref(), pretty, api_key.as_deref(), ) From a315d1cf706dfbcd4bd6050657fcff6c905dcd5c Mon Sep 17 00:00:00 2001 From: Ilya Gusev Date: Sun, 16 Aug 2026 19:32:50 +0000 Subject: [PATCH 2/2] refactor: apply simplify review to the query-time flag - carry query_time in SearchFilters instead of a 7th positional arg - bare #[arg(long)] and one-line help in the sibling style - add the flag to the README example list and the e2e suite Co-Authored-By: Claude Fable 5 --- README.md | 1 + src/commands/search.rs | 11 +++++++---- src/main.rs | 7 +++---- tests/e2e/test_search.py | 5 +++++ 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7d97c81..ced0463 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ 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" --query-time 2026-01-01T00:00:00Z # Point-in-time search keenable search "query" --api-key KEY # Use a specific API key ``` diff --git a/src/commands/search.rs b/src/commands/search.rs index 0181074..9d2a4de 100644 --- a/src/commands/search.rs +++ b/src/commands/search.rs @@ -13,6 +13,7 @@ pub struct SearchFilters { pub acquired_before: Option, pub published_after: Option, pub published_before: Option, + pub query_time: Option, } impl SearchFilters { @@ -33,6 +34,9 @@ impl SearchFilters { if let Some(v) = &self.published_before { map.insert("published_before".into(), json!(v)); } + if let Some(v) = &self.query_time { + map.insert("query_time".into(), json!(v)); + } Value::Object(map) } } @@ -286,7 +290,6 @@ pub async fn search( mode: Option<&str>, filters: SearchFilters, snippet_max_length: Option, - query_time: Option<&str>, human: bool, api_key: Option<&str>, ) { @@ -320,9 +323,6 @@ pub async fn search( if let Some(n) = snippet_max_length { body["snippet_max_length"] = json!(n); } - if let Some(qt) = query_time { - body["query_time"] = json!(qt); - } // Merge filter fields into body if let Value::Object(filter_map) = filters.to_json() && let Value::Object(ref mut body_map) = body @@ -507,6 +507,7 @@ mod tests { acquired_before: None, published_after: None, published_before: None, + query_time: None, } } @@ -520,11 +521,13 @@ mod tests { let f = SearchFilters { site: Some("docs.rs".into()), published_after: Some("2024-01-01".into()), + query_time: Some("2024-06-01T00:00:00Z".into()), ..filters() }; let v = f.to_json(); assert_eq!(v["site"], json!("docs.rs")); assert_eq!(v["published_after"], json!("2024-01-01")); + assert_eq!(v["query_time"], json!("2024-06-01T00:00:00Z")); assert!(v.get("acquired_after").is_none()); assert!(v.get("published_before").is_none()); } diff --git a/src/main.rs b/src/main.rs index 7496384..aabea7b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -171,9 +171,8 @@ enum Commands { #[arg(long = "snippet-max-length")] snippet_max_length: Option, - /// Point-in-time search: only pages available on or before this - /// timestamp (ISO 8601, e.g. 2024-01-01T00:00:00Z, or relative e.g. 7d) - #[arg(long = "query-time")] + /// Point-in-time search: only pages available on or before this timestamp (YYYY-MM-DD, ISO 8601, or relative e.g. 7d, 3mo, 1y) + #[arg(long)] query_time: Option, /// Pretty-print output for humans instead of YAML @@ -337,13 +336,13 @@ async fn main() { acquired_before, published_after, published_before, + query_time, }; commands::search::search( &query, mode.as_deref(), filters, snippet_max_length, - query_time.as_deref(), pretty, api_key.as_deref(), ) diff --git a/tests/e2e/test_search.py b/tests/e2e/test_search.py index b6ca2bb..bc2303e 100644 --- a/tests/e2e/test_search.py +++ b/tests/e2e/test_search.py @@ -155,6 +155,11 @@ def test_iso8601_datetime(kn): assert parse_ts(r["acquired_at"]) >= instant, r["url"] +def test_query_time(kn): + for r in _non_empty_results(kn, "AI news", "--query-time", "2026-05-01T00:00:00Z"): + assert parse_ts(r["acquired_at"]) <= utc(2026, 5, 1), r["url"] + + def test_combined_filters(kn): for r in _non_empty_results(kn, "async", "--site", "docs.rs", "--acquired-after", "2025-01-01"): assert host_under(host_of(r["url"]), "docs.rs"), r["url"]