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 bc04f82..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) } } @@ -503,6 +507,7 @@ mod tests { acquired_before: None, published_after: None, published_before: None, + query_time: None, } } @@ -516,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 9983728..aabea7b 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,10 @@ enum Commands { #[arg(long = "snippet-max-length")] snippet_max_length: Option, + /// 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 #[arg(short = 'p', long = "pretty")] pretty: bool, @@ -322,6 +326,7 @@ async fn main() { published_after, published_before, snippet_max_length, + query_time, pretty, api_key, } => { @@ -331,6 +336,7 @@ async fn main() { acquired_before, published_after, published_before, + query_time, }; commands::search::search( &query, 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"]