From c5b065c4356b63fee1cdc78680fc37d39ff967c5 Mon Sep 17 00:00:00 2001 From: Igor Galeta Date: Thu, 9 Jul 2026 18:52:27 +0300 Subject: [PATCH 1/6] feat: add 30 specialized SerpApi tools --- README.md | 13 +- index.ts | 88 ++++- openclaw.plugin.json | 35 +- skills/serpapi/SKILL.md | 592 +++++++++++++++++++++++++++++--- src/tools/amazon-product.ts | 97 ++++++ src/tools/amazon.ts | 101 ++++++ src/tools/autocomplete.ts | 76 ++++ src/tools/bing.ts | 108 ++++++ src/tools/duckduckgo.ts | 130 +++++++ src/tools/ebay-product.ts | 80 +++++ src/tools/ebay.ts | 141 ++++++++ src/tools/events.ts | 61 ++++ src/tools/facebook-profile.ts | 55 +++ src/tools/finance.ts | 61 ++++ src/tools/flights.ts | 85 +++++ src/tools/hotels.ts | 150 ++++++++ src/tools/immersive-product.ts | 64 ++++ src/tools/index.ts | 30 ++ src/tools/instagram-profile.ts | 62 ++++ src/tools/jobs.ts | 84 +++++ src/tools/lens.ts | 97 ++++++ src/tools/maps-reviews.ts | 143 ++++++++ src/tools/maps.ts | 90 +++++ src/tools/news.ts | 116 +++++++ src/tools/scholar.ts | 119 +++++++ src/tools/shopping.ts | 98 ++++++ src/tools/trends.ts | 105 ++++++ src/tools/tripadvisor.ts | 102 ++++++ src/tools/walmart-product.ts | 56 +++ src/tools/walmart.ts | 118 +++++++ src/tools/weather.ts | 61 ++++ src/tools/yahoo.ts | 115 +++++++ src/tools/youtube-transcript.ts | 70 ++++ src/tools/youtube-video.ts | 80 +++++ src/tools/youtube.ts | 52 +++ src/utils.ts | 20 ++ 36 files changed, 3501 insertions(+), 54 deletions(-) create mode 100644 src/tools/amazon-product.ts create mode 100644 src/tools/amazon.ts create mode 100644 src/tools/autocomplete.ts create mode 100644 src/tools/bing.ts create mode 100644 src/tools/duckduckgo.ts create mode 100644 src/tools/ebay-product.ts create mode 100644 src/tools/ebay.ts create mode 100644 src/tools/events.ts create mode 100644 src/tools/facebook-profile.ts create mode 100644 src/tools/finance.ts create mode 100644 src/tools/flights.ts create mode 100644 src/tools/hotels.ts create mode 100644 src/tools/immersive-product.ts create mode 100644 src/tools/index.ts create mode 100644 src/tools/instagram-profile.ts create mode 100644 src/tools/jobs.ts create mode 100644 src/tools/lens.ts create mode 100644 src/tools/maps-reviews.ts create mode 100644 src/tools/maps.ts create mode 100644 src/tools/news.ts create mode 100644 src/tools/scholar.ts create mode 100644 src/tools/shopping.ts create mode 100644 src/tools/trends.ts create mode 100644 src/tools/tripadvisor.ts create mode 100644 src/tools/walmart-product.ts create mode 100644 src/tools/walmart.ts create mode 100644 src/tools/weather.ts create mode 100644 src/tools/yahoo.ts create mode 100644 src/tools/youtube-transcript.ts create mode 100644 src/tools/youtube-video.ts create mode 100644 src/tools/youtube.ts create mode 100644 src/utils.ts diff --git a/README.md b/README.md index 15d1e87..1ae108c 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,14 @@ SerpApi search plugin for [OpenClaw](https://docs.openclaw.ai). Registers a - **`web_search` provider** — "SerpApi Search", backed by SerpApi's Google Light engine (fastest Google Search API). - -Specialized SerpApi verticals (news, flights, maps, shopping, scholar, finance, -YouTube, hotels, events, e-commerce, etc.) follow the same tool pattern and are -added as dedicated tools in subsequent releases, delivered through pull -requests. +- **30 specialized tools** covering Google News, Scholar, Maps (+reviews), + Shopping, Flights, Hotels, Events, Jobs, Trends, Finance, Lens, Autocomplete, + Bing, DuckDuckGo, Yahoo, YouTube (search, video, transcript), Amazon, eBay, + Walmart, Google Immersive Product, Tripadvisor, Weather, and Facebook / + Instagram public profiles. + +See [`skills/serpapi/SKILL.md`](skills/serpapi/SKILL.md) for per-tool parameters +and usage guidance. ## Install diff --git a/index.ts b/index.ts index 177666e..5d03ad1 100644 --- a/index.ts +++ b/index.ts @@ -3,14 +3,100 @@ import { type OpenClawPluginDefinition, } from "openclaw/plugin-sdk/plugin-entry"; import { createSerpApiWebSearchProvider } from "./src/serpapi-search-provider.js"; +import { + createSerpApiAmazonTool, + createSerpApiAmazonProductTool, + createSerpApiAutocompleteTool, + createSerpApiBingTool, + createSerpApiDuckDuckGoTool, + createSerpApiEbayTool, + createSerpApiEbayProductTool, + createSerpApiEventsTool, + createSerpApiFacebookProfileTool, + createSerpApiFinanceTool, + createSerpApiFlightsTool, + createSerpApiHotelsTool, + createSerpApiImmersiveProductTool, + createSerpApiInstagramProfileTool, + createSerpApiJobsTool, + createSerpApiLensTool, + createSerpApiMapsTool, + createSerpApiMapsReviewsTool, + createSerpApiNewsTool, + createSerpApiScholarTool, + createSerpApiShoppingTool, + createSerpApiTrendsTool, + createSerpApiTripadvisorTool, + createSerpApiWalmartTool, + createSerpApiWalmartProductTool, + createSerpApiWeatherTool, + createSerpApiYahooTool, + createSerpApiYouTubeTool, + createSerpApiYouTubeTranscriptTool, + createSerpApiYouTubeVideoTool, +} from "./src/tools/index.js"; const plugin: OpenClawPluginDefinition = definePluginEntry({ id: "serpapi", name: "SerpApi Search", description: - "Web search provider backed by SerpApi (Google Light). Specialized SerpApi tools for news, flights, hotels, maps, shopping, YouTube, scholar, finance, and more follow in subsequent releases.", + "Web search provider plus specialized SerpApi tools for news, flights, hotels, maps, shopping, YouTube, scholar, finance, events, and 100+ search engines.", register(api) { api.registerWebSearchProvider(createSerpApiWebSearchProvider()); + api.registerTool((ctx) => createSerpApiAmazonTool(api, ctx), { name: "serpapi_amazon" }); + api.registerTool((ctx) => createSerpApiAmazonProductTool(api, ctx), { + name: "serpapi_amazon_product", + }); + api.registerTool((ctx) => createSerpApiAutocompleteTool(api, ctx), { + name: "serpapi_autocomplete", + }); + api.registerTool((ctx) => createSerpApiBingTool(api, ctx), { name: "serpapi_bing" }); + api.registerTool((ctx) => createSerpApiDuckDuckGoTool(api, ctx), { + name: "serpapi_duckduckgo", + }); + api.registerTool((ctx) => createSerpApiEbayTool(api, ctx), { name: "serpapi_ebay" }); + api.registerTool((ctx) => createSerpApiEbayProductTool(api, ctx), { + name: "serpapi_ebay_product", + }); + api.registerTool((ctx) => createSerpApiEventsTool(api, ctx), { name: "serpapi_events" }); + api.registerTool((ctx) => createSerpApiFacebookProfileTool(api, ctx), { + name: "serpapi_facebook_profile", + }); + api.registerTool((ctx) => createSerpApiFinanceTool(api, ctx), { name: "serpapi_finance" }); + api.registerTool((ctx) => createSerpApiFlightsTool(api, ctx), { name: "serpapi_flights" }); + api.registerTool((ctx) => createSerpApiHotelsTool(api, ctx), { name: "serpapi_hotels" }); + api.registerTool((ctx) => createSerpApiImmersiveProductTool(api, ctx), { + name: "serpapi_immersive_product", + }); + api.registerTool((ctx) => createSerpApiInstagramProfileTool(api, ctx), { + name: "serpapi_instagram_profile", + }); + api.registerTool((ctx) => createSerpApiJobsTool(api, ctx), { name: "serpapi_jobs" }); + api.registerTool((ctx) => createSerpApiLensTool(api, ctx), { name: "serpapi_lens" }); + api.registerTool((ctx) => createSerpApiMapsTool(api, ctx), { name: "serpapi_maps" }); + api.registerTool((ctx) => createSerpApiMapsReviewsTool(api, ctx), { + name: "serpapi_maps_reviews", + }); + api.registerTool((ctx) => createSerpApiNewsTool(api, ctx), { name: "serpapi_news" }); + api.registerTool((ctx) => createSerpApiScholarTool(api, ctx), { name: "serpapi_scholar" }); + api.registerTool((ctx) => createSerpApiShoppingTool(api, ctx), { name: "serpapi_shopping" }); + api.registerTool((ctx) => createSerpApiTrendsTool(api, ctx), { name: "serpapi_trends" }); + api.registerTool((ctx) => createSerpApiTripadvisorTool(api, ctx), { + name: "serpapi_tripadvisor", + }); + api.registerTool((ctx) => createSerpApiWalmartTool(api, ctx), { name: "serpapi_walmart" }); + api.registerTool((ctx) => createSerpApiWalmartProductTool(api, ctx), { + name: "serpapi_walmart_product", + }); + api.registerTool((ctx) => createSerpApiWeatherTool(api, ctx), { name: "serpapi_weather" }); + api.registerTool((ctx) => createSerpApiYahooTool(api, ctx), { name: "serpapi_yahoo" }); + api.registerTool((ctx) => createSerpApiYouTubeTool(api, ctx), { name: "serpapi_youtube" }); + api.registerTool((ctx) => createSerpApiYouTubeTranscriptTool(api, ctx), { + name: "serpapi_youtube_transcript", + }); + api.registerTool((ctx) => createSerpApiYouTubeVideoTool(api, ctx), { + name: "serpapi_youtube_video", + }); }, }); diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 6a0db36..380a0d9 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -1,7 +1,7 @@ { "id": "serpapi", "name": "SerpApi Search", - "description": "Web search provider backed by SerpApi (Google Light, with 100+ engines). Specialized SerpApi tools follow in subsequent releases.", + "description": "Web search provider plus specialized SerpApi tools for news, flights, hotels, maps, shopping, YouTube, scholar, finance, events, and 100+ search engines.", "activation": { "onStartup": false }, @@ -29,7 +29,38 @@ }, "contracts": { "webSearchProviders": ["serpapi"], - "tools": [] + "tools": [ + "serpapi_amazon", + "serpapi_amazon_product", + "serpapi_autocomplete", + "serpapi_bing", + "serpapi_duckduckgo", + "serpapi_ebay", + "serpapi_ebay_product", + "serpapi_events", + "serpapi_facebook_profile", + "serpapi_finance", + "serpapi_flights", + "serpapi_hotels", + "serpapi_immersive_product", + "serpapi_instagram_profile", + "serpapi_jobs", + "serpapi_lens", + "serpapi_maps", + "serpapi_maps_reviews", + "serpapi_news", + "serpapi_scholar", + "serpapi_shopping", + "serpapi_trends", + "serpapi_tripadvisor", + "serpapi_walmart", + "serpapi_walmart_product", + "serpapi_weather", + "serpapi_yahoo", + "serpapi_youtube", + "serpapi_youtube_transcript", + "serpapi_youtube_video" + ] }, "configSchema": { "type": "object", diff --git a/skills/serpapi/SKILL.md b/skills/serpapi/SKILL.md index 7d2ad12..b5591cd 100644 --- a/skills/serpapi/SKILL.md +++ b/skills/serpapi/SKILL.md @@ -1,64 +1,564 @@ --- name: serpapi -description: SerpApi web search for OpenClaw. Real-time Google results (Google Light engine) plus access to 100+ SerpApi search engines. Specialized SerpApi tools follow in subsequent releases. +description: SerpApi search tools covering web, news, flights, hotels, maps, shopping, YouTube, scholar, finance, events, trends, weather, e-commerce, and more. metadata: { "openclaw": { "emoji": "🔎", "requires": { "config": ["plugins.entries.serpapi.enabled"] } } } --- -# SerpApi Search - -SerpApi gives OpenClaw real-time access to Google and 100+ other search engines. -This plugin registers a `web_search` provider backed by SerpApi's Google Light -engine (the fastest Google Search API). - -Specialized SerpApi verticals (news, flights, hotels, maps, shopping, scholar, -finance, YouTube, events, e-commerce, and more) are added as dedicated tools in -subsequent releases. +# SerpApi Tools ## Configuration -Set the API key either through config or the `SERPAPI_API_KEY` environment -variable. +Set your API key and optional default language under `config.webSearch`: -```json5 +```json { - plugins: { - entries: { - serpapi: { - enabled: true, - config: { - webSearch: { - apiKey: "YOUR_SERPAPI_KEY", - // Optional default language for all searches (e.g. en, de, uk). - hl: "en", - }, - }, - }, - }, - }, + "plugins": { + "entries": { + "serpapi": { + "config": { + "webSearch": { + "apiKey": "your-serpapi-key", + "hl": "en" + } + } + } + } + } } ``` -Environment fallback: +`apiKey` can also be provided via the `SERPAPI_API_KEY` environment variable. `hl` defaults to `en`. -```bash -export SERPAPI_API_KEY="YOUR_SERPAPI_KEY" -``` +## When to use which tool + +| Need | Tool | +| -------------------------------- | ---------------------------- | +| Web search | `web_search` | +| Recent news articles | `serpapi_news` | +| Academic papers / citations | `serpapi_scholar` | +| Local businesses / places | `serpapi_maps` | +| Reviews for a place | `serpapi_maps_reviews` | +| Product prices / shopping | `serpapi_shopping` | +| Amazon product search | `serpapi_amazon` | +| Amazon product details by ASIN | `serpapi_amazon_product` | +| eBay product search | `serpapi_ebay` | +| eBay product details by ID | `serpapi_ebay_product` | +| Walmart product search | `serpapi_walmart` | +| Walmart product details by ID | `serpapi_walmart_product` | +| Google Shopping product detail | `serpapi_immersive_product` | +| Job listings | `serpapi_jobs` | +| YouTube videos / channels | `serpapi_youtube` | +| YouTube video metadata | `serpapi_youtube_video` | +| YouTube video transcript | `serpapi_youtube_transcript` | +| Search trend data | `serpapi_trends` | +| Flight search | `serpapi_flights` | +| Hotel search | `serpapi_hotels` | +| Local events / concerts | `serpapi_events` | +| Stock / crypto / FX | `serpapi_finance` | +| Bing web search | `serpapi_bing` | +| DuckDuckGo web search | `serpapi_duckduckgo` | +| Yahoo! web search | `serpapi_yahoo` | +| Query autocomplete suggestions | `serpapi_autocomplete` | +| Reverse image search | `serpapi_lens` | +| Tripadvisor places / restaurants | `serpapi_tripadvisor` | +| Facebook public profile | `serpapi_facebook_profile` | +| Instagram public profile | `serpapi_instagram_profile` | +| Weather forecast | `serpapi_weather` | ## web_search -The provider-backed web search uses SerpApi's Google Light engine. It returns -titles, URLs, snippets, related questions, and related searches. - -Parameters: - -- `query` (required) — search query string. -- `count` (1-10) — number of results to return. -- `gl` — country code for Google Search (e.g. `us`, `de`, `ua`). Defaults to `us`. -- `location` — location to originate the search from (e.g. `Austin, Texas`). - Cannot be combined with `uule`. -- `uule` — Google encoded location string. Cannot be combined with `location`. -- `google_domain` — Google domain to use (e.g. `google.com`, `google.de`). -- `lr` — limit results to specific languages (e.g. `lang_en|lang_de`). -- `safe` — SafeSearch level: `active` or `off`. -- `start` — result offset for pagination (0, 10, 20, ...). +SerpApi powers this automatically when selected as the search provider. Use for +general web queries where no specialized tool is needed. + +## serpapi_news + +Search Google News for recent articles. + +| Parameter | Description | +| ------------------- | -------------------------------------------------------------- | +| `query` | News query. Append `when:7d` for recency, e.g. `"AI when:7d"`. | +| `count` | Number of results (1–10, default: 5) | +| `so` | Sort: `0`=relevance (default), `1`=date | +| `gl` | Country code (e.g. `us`, `ua`) | +| `hl` | Language code (e.g. `en`, `uk`) | +| `topic_token` | Browse a topic — use value from `menu_links[].topic_token` | +| `publication_token` | Filter by publisher — use value from `related_publications[]` | +| `section_token` | Sub-section of a topic or publisher | +| `story_token` | Full coverage of a specific story | + +Response also includes `menu_links` (topic navigation), `related_topics`, and `related_publications` — these contain tokens for follow-up calls. + +### Tips + +- `q` is optional when using `topic_token`, `publication_token`, or `story_token`. +- Use `story_token` to get full coverage of a breaking news event. + +## serpapi_scholar + +Search Google Scholar for academic papers. + +| Parameter | Description | +| --------- | ---------------------------------------------------------------- | +| `query` | Academic query. Optional when using `cites` or `cluster`. | +| `count` | Results per page (1–20, default: 5) | +| `as_ylo` | Filter from year (e.g. `2020`) | +| `as_yhi` | Filter until year | +| `scisbd` | Sort: `0`=relevance (default), `1`=date | +| `cites` | Find papers citing this article ID (from `result_id` in results) | +| `cluster` | Find all versions of this article ID | +| `as_sdt` | `0`=no patents (default), `7`=include patents, `4`=US case law | +| `lr` | Language restriction, pipe-separated (e.g. `"lang_en\|lang_de"`) | +| `start` | Pagination offset (0, 10, 20…) | + +Results include `inline_links.cited_by.total` (citation count) and `result_id` for follow-up `cites`/`cluster` calls. + +## serpapi_maps + +Find local businesses and places via Google Maps. + +| Parameter | Description | +| ---------- | -------------------------------------------------------------------- | +| `query` | Place or business type query | +| `ll` | GPS coordinates `@lat,lng,zoom` (e.g. `"@40.7128,-74.006,14z"`) | +| `location` | City or area string (e.g. `"Austin, Texas"`) | +| `nearby` | Force results near this location — use when query contains "near me" | +| `count` | Number of results (1–20, default: 5) | +| `start` | Pagination offset (0, 20, 40…, max recommended: 100) | +| `gl` | Country code | + +Response includes `serpapi_pagination.next` for fetching the next page. + +### Tips + +- Use `ll` for precise GPS-based search. +- Use `nearby` when the query contains "near me" phrases. + +## serpapi_amazon + +Search Amazon for products across any marketplace. + +| Parameter | Description | +| --------------- | --------------------------------------------------------------------------------------------------------- | +| `query` | Product search query | +| `amazon_domain` | Marketplace domain (e.g. `amazon.com`, `amazon.de`, `amazon.co.uk`, `amazon.co.jp`) | +| `language` | Locale code (e.g. `en_US`, `de_DE`, `ja_JP`) | +| `s` | Sort: `price-asc-rank`, `price-desc-rank`, `review-rank`, `date-desc-rank`, `exact-aware-popularity-rank` | +| `node` | Category node ID (from Amazon URL or `filters[].node` in a previous response) | +| `rh` | Attribute filter string from `filters[].rh` in a previous response | +| `page` | Page number for pagination (default: 1) | + +Response includes `organic_results` (with `asin`, `title`, `price`, `rating`, `reviews`, `prime`, `delivery`), `filters`, and `serpapi_pagination`. + +### Tips + +- Use `amazon_domain` + `language` together for non-US marketplaces. +- Use `node` or `rh` from a previous response's `filters` to narrow by category or attribute. +- Each `organic_results[].asin` can be used with the Amazon Product API for full product details. + +## serpapi_shopping + +Search Google Shopping for products. + +| Parameter | Description | +| --------------- | ------------------------------------------------------------------- | +| `query` | Product search query | +| `count` | Number of results (1–20, default: 5) | +| `min_price` | Minimum price filter | +| `max_price` | Maximum price filter | +| `sort_by` | `1`=price low→high, `2`=price high→low | +| `free_shipping` | `true` to show only free shipping | +| `on_sale` | `true` to show only sale items | +| `shoprs` | Filter token from `filters[].options[].shoprs` in a previous result | +| `start` | Pagination offset (0, 60, 120…) | +| `gl` | Country code | + +Response includes `filters` (with `shoprs` tokens for category refinement) and `serpapi_pagination`. + +## serpapi_jobs + +Search Google Jobs for job listings. + +| Parameter | Description | +| ----------------- | ------------------------------------------------------------------ | +| `query` | Job search query (e.g. `"software engineer remote"`) | +| `count` | Number of results (1–10, default: 5) | +| `location` | Location string (e.g. `"New York, NY"`) | +| `lrad` | Search radius in kilometers | +| `uds` | Filter string from `filters[].parameters.uds` in a previous result | +| `next_page_token` | Token for next page (from `serpapi_pagination`) | + +Response includes `filters` (with `uds` values for filtering) and `serpapi_pagination.next_page_token`. Up to 10 results per page. + +### Tips + +- Use `uds` from a previous response's `filters` array to apply job type/date filters. + +## serpapi_youtube + +Search YouTube for videos, channels, and shorts. + +| Parameter | Description | +| --------- | ---------------------------------------------------------- | +| `query` | YouTube search query | +| `sp` | YouTube filter token (copy `sp` from a YouTube search URL) | + +Response includes `video_results`, `channel_results` (name, subscribers, handle), and `shorts_results`. + +## serpapi_trends + +Get Google Trends data. + +| Parameter | Description | +| ----------- | ----------------------------------------------------------------------------------- | +| `query` | Search term(s). Up to 5 comma-separated for TIMESERIES/GEO_MAP. | +| `data_type` | `TIMESERIES` (default), `GEO_MAP`, `GEO_MAP_0`, `RELATED_TOPICS`, `RELATED_QUERIES` | +| `geo` | Region code (e.g. `US`, `UA`). Omit for worldwide. | +| `date` | Range: `now 1-d`, `now 7-d`, `today 1-m`, `today 12-m` (default), `today 5-y` | +| `region` | Breakdown level for GEO_MAP: `COUNTRY`, `REGION`, `DMA`, `CITY` | +| `gprop` | Property: `web` (default), `images`, `news`, `froogle`, `youtube`. Omit for web. | +| `cat` | Category ID (default: `0` = all) | +| `tz` | Timezone offset in minutes (e.g. `-540` Tokyo, `420` PDT) | + +Response includes `interest_over_time`, `interest_by_region`, `related_topics`, `related_queries` (whichever applies to the `data_type`). + +## serpapi_flights + +Search Google Flights for itineraries. + +| Parameter | Description | +| --------------- | ----------------------------------------- | +| `departure_id` | Departure IATA code (e.g. `JFK`, `KBP`) | +| `arrival_id` | Arrival IATA code (e.g. `LAX`, `LHR`) | +| `outbound_date` | Date YYYY-MM-DD | +| `return_date` | Return date YYYY-MM-DD (omit for one-way) | +| `type` | `1`=round trip (default), `2`=one-way | +| `adults` | Number of adult passengers (default: `1`) | +| `currency` | Currency code (default: `USD`) | +| `gl` | Country code | + +Response includes `best_flights`, `other_flights` (each with `flights[]` legs, `price`, `total_duration`, `booking_token`), and `price_insights`. + +## serpapi_hotels + +Search Google Hotels for accommodation. + +| Parameter | Description | +| ------------------ | -------------------------------------------------------- | +| `query` | Destination (e.g. `"Paris, France"`) | +| `check_in_date` | YYYY-MM-DD (default: tomorrow) | +| `check_out_date` | YYYY-MM-DD (default: check-in + 2 nights) | +| `adults` | Number of adults (default: `1`) | +| `currency` | Currency code (e.g. `USD`, `EUR`) | +| `gl` | Country code | +| `sort_by` | `3`=lowest price, `8`=highest rating, `13`=most reviewed | +| `min_price` | Minimum price per night | +| `max_price` | Maximum price per night | +| `hotel_class` | Star rating filter, comma-separated (e.g. `"4,5"`) | +| `rating` | Minimum rating: `7`=3.5+, `8`=4.0+, `9`=4.5+ | +| `vacation_rentals` | `true` to search vacation rentals instead of hotels | +| `next_page_token` | Token for next page of results | + +Response includes `properties` and `ads` (sponsored listings). + +## serpapi_events + +Search Google Events for local events. + +| Parameter | Description | +| ---------- | ------------------------------------------------------------------------------------------------------- | +| `query` | Event query (e.g. `"concerts in Austin"`) | +| `location` | Location string (e.g. `"Austin, Texas"`) | +| `htichips` | Date filter: `date:today`, `date:tomorrow`, `date:week`, `date:weekend`, `date:next_week`, `date:month` | +| `start` | Pagination offset (0, 10, 20…) | +| `gl` | Country code | +| `hl` | Language code | + +## serpapi_finance + +Look up stock prices, cryptocurrency, FX rates, and market data via Google Finance. + +| Parameter | Description | +| --------- | ----------------------------------------------------------------------- | +| `query` | Ticker or pair (e.g. `AAPL`, `BTC-USD`, `EUR-USD`, `NASDAQ:GOOGL`) | +| `window` | Time window: `1D` (default), `5D`, `1M`, `6M`, `YTD`, `1Y`, `5Y`, `MAX` | + +Response includes `summary` (price, movement, exchange), `markets` (US/Europe/Asia indices, currencies, crypto), `graph` (price history points), `knowledge_graph` (key stats), `financials` (income statement), `news_results`, and `discover_more`. + +## serpapi_maps_reviews + +Fetch reviews for a place on Google Maps. + +| Parameter | Description | +| ----------------- | ----------------------------------------------------------------------------------------- | +| `data_id` | Google Maps data ID from `serpapi_maps` results. Either `data_id` or `place_id` required. | +| `place_id` | Google Maps place ID. Either `place_id` or `data_id` required. | +| `hl` | Language code (e.g. `en`, `uk`) | +| `sort_by` | `qualityScore` (default), `newestFirst`, `ratingHigh`, `ratingLow` | +| `topic_id` | Filter by topic ID from `topics[]` in the response. Cannot be used with `query`. | +| `query` | Text filter for reviews. Cannot be used with `topic_id`. | +| `num` | Number of reviews to return (1–20, default: 10) | +| `next_page_token` | Pagination token from `serpapi_pagination.next_page_token` | + +Response includes `place_info`, `topics`, `reviews` (rating, snippet, user, date), and `serpapi_pagination`. + +### Tips + +- Obtain `data_id` from `serpapi_maps` results. +- Use `topics[]` from the first response to pass `topic_id` for filtered follow-up calls. + +## serpapi_immersive_product + +Fetch detailed Google Shopping product info including all seller prices. + +| Parameter | Description | +| ----------------- | ------------------------------------------------------------------- | +| `page_token` | Token from `serpapi_link` in a `serpapi_shopping` result. Required. | +| `next_page_token` | Token for next page of stores (from `stores_next_page_token`) | +| `more_stores` | `true` to expand additional stores | + +Response includes `product_results` with title, brand, rating, price range, all store listings, thumbnails, reviews, and more. + +## serpapi_bing + +Search the web using Bing. + +| Parameter | Description | +| ------------ | ------------------------------------------------------------------------ | +| `query` | Search query. Supports Bing operators: NOT, OR, site:, filetype:, near:. | +| `mkt` | Market locale (e.g. `en-US`, `de-DE`). Takes precedence over `cc`. | +| `cc` | 2-letter country code (e.g. `us`, `de`). Cannot be used with `mkt`. | +| `location` | Location string (e.g. `"Seattle, Washington"`) | +| `safeSearch` | `Off`, `Moderate` (default), or `Strict` (case-sensitive) | +| `first` | Result offset: `1` (default), `11` page 2, `21` page 3, ... | + +## serpapi_duckduckgo + +Search the web using DuckDuckGo. + +| Parameter | Description | +| --------------- | -------------------------------------------------------------------------------- | +| `query` | Search query | +| `kl` | Region code (e.g. `us-en`, `de-de`, `ua-uk`) | +| `safe` | SafeSearch: `1`=Strict, `-1`=Moderate, `-2`=Off | +| `df` | Date filter: `d`=day, `w`=week, `m`=month, `y`=year, or `YYYY-MM-DD..YYYY-MM-DD` | +| `m` | Max results (1–50) | +| `start` | Pagination offset | +| `search_assist` | `true` to enable search assist suggestions | + +Response includes `results`, `knowledge_graph`, `news_results`, `related_searches`, `search_assist`. + +## serpapi_yahoo + +Search the web using Yahoo!. + +| Parameter | Description | +| -------------- | ----------------------------------------------------------- | +| `query` | Search query | +| `yahoo_domain` | Domain prefix (e.g. `fr` for fr.search.yahoo.com) | +| `vc` | 2-letter country code (e.g. `us`, `gb`, `fr`) | +| `vl` | Language filter (e.g. `lang_fr` to search French only) | +| `vm` | Adult filter: `r`=Strict, `i`=Moderate, `p`=Off | +| `vs` | TLD filter, comma-separated (e.g. `.com,.org`) | +| `vf` | File format (e.g. `pdf`, `txt`) | +| `b` | Pagination offset (default: 1, page 2: 11, page 3: 21, ...) | + +## serpapi_autocomplete + +Get Google search query completions. + +| Parameter | Description | +| --------- | ----------------------------------------------------------------- | +| `query` | Partial search query to get completions for | +| `gl` | Country code (e.g. `us`, `ua`) | +| `hl` | Language code (e.g. `en`, `uk`) | +| `cp` | Cursor position (0-based, defaults to end of query) | +| `client` | Autocomplete client: `chrome`, `safari`, `firefox-b-d`, `youtube` | + +Response includes `suggestions[]` with `value`, `relevance`, and `type`, plus `verbatim_relevance`. + +## serpapi_lens + +Perform a Google Lens reverse image search. + +| Parameter | Description | +| ----------- | ---------------------------------------------------------------------------------- | +| `url` | Public URL of the image to search. Required. | +| `type` | `all` (default), `about_this_image`, `products`, `exact_matches`, `visual_matches` | +| `q` | Optional query to refine results (applies to `all`, `visual_matches`, `products`) | +| `hl` | Language code | +| `country` | 2-letter country code | +| `safe` | `active` or `off` | +| `auto_crop` | `true` to auto-crop to detected area of interest | + +Response includes `visual_matches`, `exact_matches`, `related_content`, `knowledge_graph`, `text_results`, `ai_overview`. + +## serpapi_youtube_video + +Fetch metadata for a YouTube video. + +| Parameter | Description | +| ----------------- | ------------------------------------------------------------------------- | +| `v` | YouTube video ID (e.g. `dQw4w9WgXcQ`). Required. | +| `hl` | Language code | +| `gl` | Country code | +| `next_page_token` | Paginate related videos or comments using tokens from a previous response | + +Response includes `title`, `channel`, `views`, `likes`, `description`, `chapters`, `related_videos`, pagination tokens (`related_videos_next_page_token`, `comments_next_page_token`, `comments_sorting_token`), and a `transcript` link. + +### Tips + +- Use `related_videos_next_page_token` to page through related videos. +- Use `comments_next_page_token` or `comments_sorting_token[].token` to fetch comments. +- Use `serpapi_youtube_transcript` to get the full text transcript. + +## serpapi_youtube_transcript + +Fetch the transcript of a YouTube video. + +| Parameter | Description | +| --------------- | ----------------------------------------------------------------------------------------------- | +| `v` | YouTube video ID. Required. | +| `language_code` | Language code (e.g. `en`, `es-ES`, `zh-Hans`). Defaults to `en`. Falls back to first available. | +| `title` | Select a specific transcript by title (e.g. `"Twitch Chat - Simple"`) | +| `type` | Transcript type: `asr` for auto-generated | + +Response includes `transcript[]` (timestamped segments), `video_id`, `title`, `language_code`. + +## serpapi_amazon_product + +Fetch detailed Amazon product info by ASIN. + +| Parameter | Description | +| ------------------- | ------------------------------------------------------------------------- | +| `asin` | Amazon ASIN (e.g. `B072MQ5BRX` from amazon.com/dp/B072MQ5BRX). Required. | +| `amazon_domain` | Marketplace (e.g. `amazon.co.uk`, `amazon.de`). Defaults to `amazon.com`. | +| `language` | Locale (e.g. `en_US`, `es_US`, `ja_JP`) | +| `delivery_zip` | ZIP/postal code for shipping availability | +| `shipping_location` | Country for shipping filtering | + +Response includes `product` (title, price, rating, specs, variants, images), `purchase_options`, `related_products`, `bought_together`, `reviews_information`. + +### Tips + +- Get `asin` values from `serpapi_amazon` results. + +## serpapi_ebay + +Search eBay listings. + +| Parameter | Description | +| --------------- | ----------------------------------------------------------------------------------------- | +| `query` | Search query. Optional when `category_id` is set. | +| `ebay_domain` | eBay domain (e.g. `ebay.co.uk`, `ebay.de`). Defaults to `ebay.com`. | +| `buying_format` | `Auction`, `BIN` (Buy It Now), or `BO` (Accepts Offers) | +| `show_only` | Comma-separated flags: `Sold`, `FS` (Free shipping), `FR` (Free returns), `LPickup`, etc. | +| `min_price` | Minimum price | +| `max_price` | Maximum price | +| `sort` | Sort numeric code (see serpapi.com/ebay-sort-options) | +| `category_id` | Category ID from `categories[]` in a previous result | +| `condition` | Condition ID(s): `1000`=New, `3000`=Used. Combine with `\|` (e.g. `1000\|3000`) | +| `zip` | ZIP/postal code for local shipping | +| `page` | Page number (default: 1) | +| `per_page` | Results per page: `25`, `50` (default), `100`, `200` | + +Response includes `organic_results`, `filters`, `categories`, `serpapi_pagination`. + +### Tips + +- Use `organic_results[].product_id` with `serpapi_ebay_product` for full listing details. + +## serpapi_ebay_product + +Fetch detailed eBay listing info by product ID. + +| Parameter | Description | +| ------------------ | --------------------------------------------------------------------------------- | +| `product_id` | eBay item ID from the URL (e.g. `30557685` from ebay.com/itm/30557685). Required. | +| `ebay_domain` | eBay domain (e.g. `ebay.co.uk`). Defaults to `ebay.com`. | +| `locale` | Locale for search origin | +| `lang` | Language override (US domain + locale only) | +| `shipping_country` | Country code for shipping cost calculation | + +Response includes `product_results` (title, price, condition, specs, shipping, returns, media), `seller_results`, `related_products`. + +## serpapi_tripadvisor + +Search Tripadvisor for destinations, hotels, restaurants, and attractions. + +| Parameter | Description | +| -------------------- | ------------------------------------------------------------------------------------------------------ | +| `query` | Search query (e.g. `"Rome"`, `"best restaurants in Paris"`) | +| `ssrc` | Filter: `a`=All (default), `r`=Restaurants, `A`=Things to Do, `h`=Hotels, `g`=Destinations, `f`=Forums | +| `tripadvisor_domain` | Domain (e.g. `www.tripadvisor.co.uk`). Defaults to `tripadvisor.com`. | +| `lat` / `lon` | GPS coordinates for location-based search | +| `limit` | Max results (1–100, default: 30) | +| `offset` | Pagination offset (0, 30, 60, ...) | + +Response includes `places`, `restaurants`, `hotels`, `attractions`, `serpapi_pagination`. + +## serpapi_facebook_profile + +Fetch a public Facebook profile. + +| Parameter | Description | +| ------------ | ------------------------------------------------------------------------ | +| `profile_id` | Profile slug or numeric ID (e.g. `Meta` or `100080376596424`). Required. | + +Response includes `profile`, `posts`, `photos`, `videos`, `about`. + +## serpapi_instagram_profile + +Fetch a public Instagram profile. + +| Parameter | Description | +| ----------------- | ------------------------------------------------------------------------------------ | +| `profile_id` | Profile ID from the URL (e.g. `serpapicom` from instagram.com/serpapicom). Required. | +| `next_page_token` | Pagination token from `serpapi_pagination.next_page_token` from a previous response. | + +Response includes `profile`, `posts`, `reels`, `highlights`, `related_profiles`, and `serpapi_pagination`. + +## serpapi_weather + +Get weather for a location via Google. + +| Parameter | Description | +| --------- | ----------------------------------------------------------------------------------------- | +| `query` | Natural language query (e.g. `"weather in Kyiv"`, `"forecast Paris tomorrow"`). Required. | +| `gl` | Country code (e.g. `us`, `ua`) | +| `hl` | Language code (e.g. `en`, `uk`) | + +Response includes `answer_box` with `high`, `low`, `weather` (conditions), `date`, `location`, and `icon`. + +## serpapi_walmart + +Search Walmart product listings. + +| Parameter | Description | +| ---------------- | ---------------------------------------------------------------------------- | +| `query` | Search query. Optional when `cat_id` is set. | +| `walmart_domain` | Domain (e.g. `walmart.ca`, `walmart.com.mx`). Defaults to `walmart.com`. | +| `sort` | `price_low`, `price_high`, `best_seller`, `best_match`, `rating_high`, `new` | +| `cat_id` | Category ID (e.g. `0` for all). Either `query` or `cat_id` required. | +| `facet` | Attribute filter: `key:value` pairs separated by `\|\|` | +| `store_id` | Filter by specific Walmart store | +| `min_price` | Minimum price | +| `max_price` | Maximum price | +| `page` | Page number (default: 1, max: 100) | + +Response includes `search_information`, `organic_results` (with `us_item_id`, `product_id`, price, rating), `serpapi_pagination`. + +### Tips + +- Use `organic_results[].product_id` with `serpapi_walmart_product` for full product details. + +## serpapi_walmart_product + +Fetch detailed Walmart product info by product ID. + +| Parameter | Description | +| ------------ | ------------------------------------------------------------------------- | +| `product_id` | Walmart product ID or `us_item_id` from URL (e.g. `138762768`). Required. | +| `store_id` | Store ID for store-specific pricing | + +Response includes `product_result` (title, price, specs, stock, shipping/pickup/delivery options) and `reviews_results`. diff --git a/src/tools/amazon-product.ts b/src/tools/amazon-product.ts new file mode 100644 index 0000000..3f77990 --- /dev/null +++ b/src/tools/amazon-product.ts @@ -0,0 +1,97 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { jsonResult, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = [ + "asin", + "amazon_domain", + "language", + "delivery_zip", + "shipping_location", + "other_sellers", + "zero_trace", +] as const; + +function extract(raw: Record): Record { + const { + purchase_options, + related_products, + bought_together, + reviews_information, + product_results, + } = raw as { + purchase_options?: unknown; + related_products?: unknown; + bought_together?: unknown; + reviews_information?: unknown; + product_results?: Record; + }; + return { + engine: "amazon_product", + product: product_results ?? null, + purchase_options: purchase_options ?? null, + related_products: related_products ?? [], + bought_together: bought_together ?? [], + reviews_information: reviews_information ?? null, + }; +} + +export function createSerpApiAmazonProductTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_amazon_product", + label: "SerpApi Amazon Product", + description: + "Fetch detailed product information from Amazon by ASIN via SerpApi. " + + "Returns title, price, rating, reviews, specifications, variants, delivery info, and related products. " + + "Use serpapi_amazon to search for products and get ASINs first.", + parameters: { + type: "object", + properties: { + asin: { + type: "string", + description: + "Amazon Standard Identification Number (ASIN) of the product. " + + "Found in the Amazon product URL (e.g. 'B072MQ5BRX' from amazon.com/dp/B072MQ5BRX).", + }, + amazon_domain: { + type: "string", + description: + "Amazon domain to use (e.g. amazon.co.uk, amazon.de). Defaults to amazon.com.", + }, + language: { + type: "string", + description: "Language locale for the product page (e.g. en_US, es_US, ja_JP).", + }, + delivery_zip: { + type: "string", + description: "ZIP/postal code to filter shipping availability by location.", + }, + shipping_location: { + type: "string", + description: "Country to filter shipping products by.", + }, + }, + required: ["asin"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const raw = await callSerpApi({ + cfg, + engine: "amazon_product", + allowedParams: ALLOWED_PARAMS, + params: { + asin: readStringParam(args, "asin", { required: true }), + amazon_domain: readStringParam(args, "amazon_domain") ?? undefined, + language: readStringParam(args, "language") ?? undefined, + delivery_zip: readStringParam(args, "delivery_zip") ?? undefined, + shipping_location: readStringParam(args, "shipping_location") ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/amazon.ts b/src/tools/amazon.ts new file mode 100644 index 0000000..044e2a4 --- /dev/null +++ b/src/tools/amazon.ts @@ -0,0 +1,101 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { + jsonResult, + readNumberParam, + readStringParam, +} from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = [ + "k", + "amazon_domain", + "language", + "s", + "node", + "rh", + "page", + "zero_trace", +] as const; + +function extract(raw: Record): Record { + return { + engine: "amazon", + results: Array.isArray(raw.organic_results) ? raw.organic_results : [], + filters: raw.filters ?? {}, + serpapi_pagination: raw.serpapi_pagination ?? null, + }; +} + +export function createSerpApiAmazonTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_amazon", + label: "SerpApi Amazon Search", + description: + "Search Amazon for products. Returns titles, prices, ratings, Prime status, ASIN, and delivery info. " + + "Use amazon_domain to target specific marketplaces (e.g. amazon.de, amazon.co.uk).", + parameters: { + type: "object", + properties: { + query: { type: "string", description: "Product search query." }, + amazon_domain: { + type: "string", + description: + "Amazon marketplace domain (e.g. amazon.com, amazon.de, amazon.co.uk, amazon.co.jp).", + }, + language: { + type: "string", + description: "Locale code (e.g. en_US, de_DE, ja_JP).", + }, + s: { + type: "string", + enum: [ + "relevanceblender", + "price-asc-rank", + "price-desc-rank", + "review-rank", + "date-desc-rank", + "exact-aware-popularity-rank", + ], + description: + "Sort: relevanceblender=Featured (default), price-asc-rank, price-desc-rank, review-rank, date-desc-rank=Newest, exact-aware-popularity-rank=Best Sellers.", + }, + node: { + type: "string", + description: "Category node ID to filter results (from Amazon URL or filters[].node).", + }, + rh: { + type: "string", + description: "Attribute filter string from filters[].rh in a previous response.", + }, + page: { + type: "number", + description: "Page number for pagination (default: 1).", + minimum: 1, + }, + }, + required: ["query"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const raw = await callSerpApi({ + cfg, + engine: "amazon", + allowedParams: ALLOWED_PARAMS, + params: { + k: readStringParam(args, "query", { required: true }), + amazon_domain: readStringParam(args, "amazon_domain") ?? undefined, + language: readStringParam(args, "language") ?? undefined, + s: readStringParam(args, "s") ?? undefined, + node: readStringParam(args, "node") ?? undefined, + rh: readStringParam(args, "rh") ?? undefined, + page: readNumberParam(args, "page", { integer: true }) ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/autocomplete.ts b/src/tools/autocomplete.ts new file mode 100644 index 0000000..836604c --- /dev/null +++ b/src/tools/autocomplete.ts @@ -0,0 +1,76 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { + jsonResult, + readNumberParam, + readStringParam, +} from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = ["q", "gl", "hl", "cp", "client", "zero_trace"] as const; + +function extract(raw: Record): Record { + return { + engine: "google_autocomplete", + suggestions: raw.suggestions ?? [], + verbatim_relevance: raw.verbatim_relevance ?? null, + }; +} + +export function createSerpApiAutocompleteTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_autocomplete", + label: "SerpApi Google Autocomplete", + description: + "Fetch Google search autocomplete suggestions for a partial query via SerpApi. " + + "Returns a ranked list of query completions with relevance scores. " + + "Useful for query expansion, spell suggestions, and search UX features.", + parameters: { + type: "object", + properties: { + query: { + type: "string", + description: "Partial search query to get completions for.", + }, + gl: { + type: "string", + description: "Two-letter country code (e.g. us, gb, fr).", + }, + hl: { + type: "string", + description: "Two-letter language code (e.g. en, es, fr).", + }, + cp: { + type: "number", + description: "Cursor position in the query string (0-based). Defaults to end of query.", + minimum: 0, + }, + client: { + type: "string", + description: + "Autocomplete client (e.g. chrome, safari, firefox-b-d, youtube). Affects suggestion style.", + }, + }, + required: ["query"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const raw = await callSerpApi({ + cfg, + engine: "google_autocomplete", + allowedParams: ALLOWED_PARAMS, + params: { + q: readStringParam(args, "query", { required: true }), + gl: readStringParam(args, "gl") ?? undefined, + hl: readStringParam(args, "hl") ?? undefined, + cp: readNumberParam(args, "cp", { integer: true }) ?? undefined, + client: readStringParam(args, "client") ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/bing.ts b/src/tools/bing.ts new file mode 100644 index 0000000..f503fe9 --- /dev/null +++ b/src/tools/bing.ts @@ -0,0 +1,108 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { + jsonResult, + readNumberParam, + readStringParam, + wrapWebContent, +} from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = [ + "q", + "cc", + "mkt", + "location", + "safeSearch", + "filters", + "first", + "zero_trace", +] as const; + +function extract(raw: Record): Record { + const organicResults = Array.isArray(raw.organic_results) + ? (raw.organic_results as Record[]) + : []; + return { + engine: "bing", + results: organicResults.map((r) => ({ + title: typeof r.title === "string" ? wrapWebContent(r.title) : (r.title ?? null), + url: r.link ?? null, + snippet: typeof r.snippet === "string" ? wrapWebContent(r.snippet) : (r.snippet ?? null), + })), + related_searches: raw.related_searches ?? [], + }; +} + +export function createSerpApiBingTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_bing", + label: "SerpApi Bing Search", + description: + "Search the web using Bing via SerpApi. Returns titles, URLs, and snippets. " + + "Use mkt for locale (e.g. en-US, de-DE) or cc for country. " + + "Useful as an alternative to Google results.", + parameters: { + type: "object", + properties: { + query: { + type: "string", + description: + "Search query string. Supports Bing operators: NOT, OR, site:, filetype:, near:.", + }, + mkt: { + type: "string", + description: + "Market code combining language and country (e.g. en-US, de-DE, fr-FR). Takes precedence over cc.", + }, + cc: { + type: "string", + description: "2-letter ISO country code (e.g. us, de, gb). Cannot be used with mkt.", + }, + location: { + type: "string", + description: "Location to originate the search from (e.g. 'Seattle, Washington').", + }, + safeSearch: { + type: "string", + enum: ["Off", "Moderate", "Strict"], + description: "SafeSearch level (default: Moderate).", + }, + first: { + type: "number", + description: + "Result offset for pagination (default: 1; use 11 for page 2, 21 for page 3, ...).", + minimum: 1, + }, + }, + required: ["query"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const mkt = readStringParam(args, "mkt"); + const cc = readStringParam(args, "cc"); + if (mkt && cc) { + throw new Error( + "serpapi_bing: mkt and cc are mutually exclusive; provide one or the other", + ); + } + const raw = await callSerpApi({ + cfg, + engine: "bing", + allowedParams: ALLOWED_PARAMS, + params: { + q: readStringParam(args, "query", { required: true }), + mkt: mkt ?? undefined, + cc: cc ?? undefined, + location: readStringParam(args, "location") ?? undefined, + safeSearch: readStringParam(args, "safeSearch") ?? undefined, + first: readNumberParam(args, "first", { integer: true }) ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/duckduckgo.ts b/src/tools/duckduckgo.ts new file mode 100644 index 0000000..09d4e0a --- /dev/null +++ b/src/tools/duckduckgo.ts @@ -0,0 +1,130 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { + jsonResult, + readNumberParam, + readStringParam, + wrapWebContent, +} from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, readBooleanArg, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = [ + "q", + "kl", + "safe", + "df", + "m", + "start", + "search_assist", + "zero_trace", +] as const; + +function extract(raw: Record): Record { + const organicResults = Array.isArray(raw.organic_results) + ? (raw.organic_results as Record[]) + : []; + const kg = raw.knowledge_graph as Record | undefined; + return { + engine: "duckduckgo", + results: organicResults.map((r) => ({ + title: typeof r.title === "string" ? wrapWebContent(r.title) : (r.title ?? null), + url: r.link ?? null, + snippet: typeof r.snippet === "string" ? wrapWebContent(r.snippet) : (r.snippet ?? null), + })), + knowledge_graph: kg + ? { + title: typeof kg.title === "string" ? wrapWebContent(kg.title) : (kg.title ?? null), + description: + typeof kg.description === "string" + ? wrapWebContent(kg.description) + : (kg.description ?? null), + website: kg.website ?? null, + facts: kg.facts ?? null, + } + : null, + news_results: raw.news_results ?? [], + related_searches: raw.related_searches ?? [], + search_assist: raw.search_assist ?? null, + }; +} + +export function createSerpApiDuckDuckGoTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_duckduckgo", + label: "SerpApi DuckDuckGo Search", + description: + "Search the web using DuckDuckGo via SerpApi. Returns organic results, knowledge graph, and news. " + + "Privacy-focused alternative to Google/Bing. Use kl for region (e.g. us-en, de-de, uk-en). " + + "Use df to filter by date: d=past day, w=past week, m=past month, y=past year.", + parameters: { + type: "object", + properties: { + query: { + type: "string", + description: "Search query. Supports DDG operators: site:, intitle:, inurl:, filetype:.", + }, + kl: { + type: "string", + description: + "Region code (e.g. us-en, uk-en, de-de, fr-fr). Controls language and region of results.", + }, + safe: { + type: "number", + enum: [1, -1, -2], + description: "SafeSearch: 1=Strict, -1=Moderate (default), -2=Off.", + }, + df: { + type: "string", + description: + "Date filter: d=past day, w=past week, m=past month, y=past year, or custom range '2024-01-01..2024-12-31'.", + }, + m: { + type: "number", + description: + "Maximum results to return (1–50, default: 50). Cannot be used with search_assist.", + minimum: 1, + maximum: 50, + }, + start: { + type: "number", + description: + "Result offset for pagination (default: 0). First page returns up to 35 results; subsequent pages up to 50.", + minimum: 0, + }, + search_assist: { + type: "boolean", + description: + "Include DuckDuckGo AI Search Assist answer in the response. Cannot be used with m.", + }, + }, + required: ["query"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const searchAssist = readBooleanArg(args, "search_assist"); + const searchAssistParam = searchAssist === true ? "true" : undefined; + const m = readNumberParam(args, "m", { integer: true }); + if (searchAssist === true && m != null) { + throw new Error("serpapi_duckduckgo: search_assist and m are mutually exclusive"); + } + const raw = await callSerpApi({ + cfg, + engine: "duckduckgo", + allowedParams: ALLOWED_PARAMS, + params: { + q: readStringParam(args, "query", { required: true }), + kl: readStringParam(args, "kl") ?? undefined, + safe: readNumberParam(args, "safe", { integer: true }) ?? undefined, + df: readStringParam(args, "df") ?? undefined, + m: m ?? undefined, + start: readNumberParam(args, "start", { integer: true }) ?? undefined, + search_assist: searchAssistParam, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/ebay-product.ts b/src/tools/ebay-product.ts new file mode 100644 index 0000000..90262d6 --- /dev/null +++ b/src/tools/ebay-product.ts @@ -0,0 +1,80 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { jsonResult, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = [ + "product_id", + "ebay_domain", + "locale", + "lang", + "shipping_country", + "zero_trace", +] as const; + +function extract(raw: Record): Record { + return { + engine: "ebay_product", + product_results: raw.product_results ?? null, + seller_results: raw.seller_results ?? null, + related_products: raw.related_products ?? [], + }; +} + +export function createSerpApiEbayProductTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_ebay_product", + label: "SerpApi eBay Product", + description: + "Fetch detailed product information from an eBay listing by product ID via SerpApi. " + + "Returns title, price, shipping, condition, specifications, seller info, and related products. " + + "Use serpapi_ebay to search for listings and get product IDs first.", + parameters: { + type: "object", + properties: { + product_id: { + type: "string", + description: + "eBay product/item ID. Found in the listing URL (e.g. '30557685' from ebay.com/itm/30557685).", + }, + ebay_domain: { + type: "string", + description: "eBay domain to use (e.g. ebay.co.uk, ebay.de). Defaults to ebay.com.", + }, + locale: { + type: "string", + description: "Locale for the search origin. See serpapi.com/ebay-locales.", + }, + lang: { + type: "string", + description: + "Language override (e.g. en-US). Only applicable on US eBay domain when locale is set.", + }, + shipping_country: { + type: "string", + description: "Country code for shipping cost calculation (e.g. US, GB, DE).", + }, + }, + required: ["product_id"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const raw = await callSerpApi({ + cfg, + engine: "ebay_product", + allowedParams: ALLOWED_PARAMS, + params: { + product_id: readStringParam(args, "product_id", { required: true }), + ebay_domain: readStringParam(args, "ebay_domain") ?? undefined, + locale: readStringParam(args, "locale") ?? undefined, + lang: readStringParam(args, "lang") ?? undefined, + shipping_country: readStringParam(args, "shipping_country") ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/ebay.ts b/src/tools/ebay.ts new file mode 100644 index 0000000..c03fa87 --- /dev/null +++ b/src/tools/ebay.ts @@ -0,0 +1,141 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { + jsonResult, + readNumberParam, + readStringParam, +} from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = [ + "_nkw", + "ebay_domain", + "_salic", + "_pgn", + "_ipg", + "show_only", + "buying_format", + "_udlo", + "_udhi", + "_sop", + "category_id", + "_stpos", + "LH_ItemCondition", + "LH_PrefLoc", + "zero_trace", +] as const; + +function extract(raw: Record): Record { + return { + engine: "ebay", + organic_results: raw.organic_results ?? [], + filters: raw.filters ?? [], + categories: raw.categories ?? [], + serpapi_pagination: raw.serpapi_pagination ?? null, + }; +} + +export function createSerpApiEbayTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_ebay", + label: "SerpApi eBay Search", + description: + "Search eBay listings via SerpApi. Returns product titles, prices, conditions, and links. " + + "Supports filtering by price range, buying format (Auction/BIN/BO), condition, and category. " + + "Use serpapi_amazon for Amazon searches.", + parameters: { + type: "object", + properties: { + query: { + type: "string", + description: "eBay search query. Optional when category_id is specified.", + }, + ebay_domain: { + type: "string", + description: "eBay domain to use (e.g. ebay.co.uk, ebay.de). Defaults to ebay.com.", + }, + buying_format: { + type: "string", + enum: ["Auction", "BIN", "BO"], + description: + "Filter by buying format: Auction, BIN (Buy It Now), or BO (Accepts Offers).", + }, + show_only: { + type: "string", + description: + "Comma-separated filter flags (case-sensitive): Sold, Complete, FR (Free returns), FS (Free shipping), " + + "RPA (Returns accepted), AS (Authorized seller), SaleItems, Lots, Charity, AV (Authenticity Guarantee), LPickup (Local pickup).", + }, + min_price: { + type: "number", + description: "Minimum price filter.", + }, + max_price: { + type: "number", + description: "Maximum price filter.", + }, + sort: { + type: "number", + description: + "Sort order numeric code. See serpapi.com/ebay-sort-options. Default is Best Match.", + }, + category_id: { + type: "string", + description: + "Category ID to narrow search. Obtain from a previous results' categories array.", + }, + condition: { + type: "string", + description: + "Product condition ID(s). Use 1000 for New, 3000 for Used. Combine multiple with | (e.g. '1000|3000').", + }, + zip: { + type: "string", + description: "ZIP or postal code to filter shipping by location.", + }, + page: { + type: "number", + description: "Page number for pagination (default: 1).", + minimum: 1, + }, + per_page: { + type: "number", + enum: [25, 50, 100, 200], + description: "Results per page: 25, 50 (default), 100, or 200.", + }, + }, + required: [], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const query = readStringParam(args, "query"); + const categoryId = readStringParam(args, "category_id"); + if (!query && !categoryId) { + throw new Error("serpapi_ebay: either query or category_id is required"); + } + const raw = await callSerpApi({ + cfg, + engine: "ebay", + allowedParams: ALLOWED_PARAMS, + params: { + _nkw: query ?? undefined, + ebay_domain: readStringParam(args, "ebay_domain") ?? undefined, + buying_format: readStringParam(args, "buying_format") ?? undefined, + show_only: readStringParam(args, "show_only") ?? undefined, + _udlo: readNumberParam(args, "min_price") ?? undefined, + _udhi: readNumberParam(args, "max_price") ?? undefined, + _sop: readNumberParam(args, "sort", { integer: true }) ?? undefined, + category_id: categoryId ?? undefined, + LH_ItemCondition: readStringParam(args, "condition") ?? undefined, + _stpos: readStringParam(args, "zip") ?? undefined, + _pgn: readNumberParam(args, "page", { integer: true }) ?? undefined, + _ipg: readNumberParam(args, "per_page", { integer: true }) ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/events.ts b/src/tools/events.ts new file mode 100644 index 0000000..ad62fb7 --- /dev/null +++ b/src/tools/events.ts @@ -0,0 +1,61 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { + jsonResult, + readNumberParam, + readStringParam, +} from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = ["q", "gl", "hl", "location", "htichips", "start", "zero_trace"] as const; + +function extract(raw: Record): Record { + const events = Array.isArray(raw.events_results) ? (raw.events_results as unknown[]) : []; + return { engine: "google_events", events }; +} + +export function createSerpApiEventsTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_events", + label: "SerpApi Google Events", + description: + "Search local events via Google Events. Returns title, date, address, and ticket links. " + + "htichips date filter: date:today, date:tomorrow, date:week, date:weekend, date:next_week, date:month.", + parameters: { + type: "object", + properties: { + query: { type: "string", description: 'Event search query (e.g. "concerts in Austin").' }, + location: { type: "string", description: 'Location string (e.g. "Austin, Texas").' }, + htichips: { + type: "string", + description: + "Date filter: date:today, date:tomorrow, date:week, date:weekend, date:next_week, date:month.", + }, + gl: { type: "string", description: "Country code (e.g. us, de, ua)." }, + hl: { type: "string", description: "Language code override." }, + start: { type: "number", description: "Result offset for pagination (0, 10, 20...)." }, + }, + required: ["query"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const raw = await callSerpApi({ + cfg, + engine: "google_events", + allowedParams: ALLOWED_PARAMS, + params: { + q: readStringParam(args, "query", { required: true }), + location: readStringParam(args, "location") ?? undefined, + htichips: readStringParam(args, "htichips") ?? undefined, + gl: readStringParam(args, "gl") ?? undefined, + hl: readStringParam(args, "hl") ?? undefined, + start: readNumberParam(args, "start", { integer: true }) ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/facebook-profile.ts b/src/tools/facebook-profile.ts new file mode 100644 index 0000000..3ea47c6 --- /dev/null +++ b/src/tools/facebook-profile.ts @@ -0,0 +1,55 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { jsonResult, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = ["profile_id", "zero_trace"] as const; + +function extract(raw: Record): Record { + return { + engine: "facebook_profile", + profile: raw.profile ?? null, + posts: raw.posts ?? [], + photos: raw.photos ?? [], + videos: raw.videos ?? [], + about: raw.about ?? null, + }; +} + +export function createSerpApiFacebookProfileTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_facebook_profile", + label: "SerpApi Facebook Profile", + description: + "Fetch a public Facebook profile via SerpApi. " + + "Returns profile info, posts, photos, videos, and about section. " + + "Use the profile slug (e.g. Meta) or numeric ID from the profile URL.", + parameters: { + type: "object", + properties: { + profile_id: { + type: "string", + description: + "Facebook profile ID or slug from the profile URL. " + + "E.g. 'Meta' from facebook.com/Meta, or '100080376596424' from facebook.com/profile.php?id=100080376596424.", + }, + }, + required: ["profile_id"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const raw = await callSerpApi({ + cfg, + engine: "facebook_profile", + allowedParams: ALLOWED_PARAMS, + params: { + profile_id: readStringParam(args, "profile_id", { required: true }), + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/finance.ts b/src/tools/finance.ts new file mode 100644 index 0000000..ee9f3ca --- /dev/null +++ b/src/tools/finance.ts @@ -0,0 +1,61 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { jsonResult, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = ["q", "hl", "window", "zero_trace"] as const; + +function extract(raw: Record): Record { + return { + engine: "google_finance", + summary: raw.summary ?? null, + markets: raw.markets ?? null, + graph: raw.graph ?? null, + knowledge_graph: raw.knowledge_graph ?? null, + financials: raw.financials ?? null, + news_results: raw.news_results ?? null, + discover_more: raw.discover_more ?? null, + }; +} + +export function createSerpApiFinanceTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_finance", + label: "SerpApi Google Finance", + description: + "Look up stock prices, currency rates, and cryptocurrency via Google Finance. " + + "Returns price, change, and recent news. " + + "Examples: q='AAPL' (Apple stock), q='BTC-USD' (Bitcoin), q='USDEUR=X' (USD/EUR rate). " + + "window: 1D, 5D, 1M, 6M, YTD, 1Y, 5Y, MAX.", + parameters: { + type: "object", + properties: { + query: { + type: "string", + description: "Ticker or pair (e.g. AAPL, BTC-USD, USDEUR=X, NASDAQ:GOOGL).", + }, + window: { + type: "string", + description: "Time window (default: 1D). Options: 1D, 5D, 1M, 6M, YTD, 1Y, 5Y, MAX.", + }, + }, + required: ["query"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const raw = await callSerpApi({ + cfg, + engine: "google_finance", + allowedParams: ALLOWED_PARAMS, + params: { + q: readStringParam(args, "query", { required: true }), + window: readStringParam(args, "window") ?? "1D", + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/flights.ts b/src/tools/flights.ts new file mode 100644 index 0000000..cf0d352 --- /dev/null +++ b/src/tools/flights.ts @@ -0,0 +1,85 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { + jsonResult, + readNumberParam, + readStringParam, +} from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = [ + "departure_id", + "arrival_id", + "outbound_date", + "return_date", + "type", + "adults", + "currency", + "gl", + "hl", + "zero_trace", +] as const; + +function extract(raw: Record): Record { + return { + engine: "google_flights", + best_flights: raw.best_flights ?? [], + other_flights: raw.other_flights ?? [], + price_insights: raw.price_insights ?? null, + }; +} + +export function createSerpApiFlightsTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_flights", + label: "SerpApi Google Flights", + description: + "Search flights via Google Flights. Returns real prices and itineraries. " + + "Use IATA codes for departure_id and arrival_id (e.g. JFK, LAX, KBP). " + + "type: 1=round trip (default), 2=one-way.", + parameters: { + type: "object", + properties: { + departure_id: { type: "string", description: "Departure airport IATA code (e.g. JFK)." }, + arrival_id: { type: "string", description: "Arrival airport IATA code (e.g. LAX)." }, + outbound_date: { type: "string", description: "Departure date YYYY-MM-DD." }, + return_date: { type: "string", description: "Return date YYYY-MM-DD (omit for one-way)." }, + type: { + type: "string", + enum: ["1", "2"], + description: "1 = round trip (default), 2 = one-way.", + }, + adults: { + type: "number", + description: "Number of adult passengers (default: 1).", + minimum: 1, + }, + currency: { type: "string", description: "Currency code (default: USD)." }, + gl: { type: "string", description: "Country code (e.g. us, de, ua)." }, + }, + required: ["departure_id", "arrival_id", "outbound_date"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const raw = await callSerpApi({ + cfg, + engine: "google_flights", + allowedParams: ALLOWED_PARAMS, + params: { + departure_id: readStringParam(args, "departure_id", { required: true }), + arrival_id: readStringParam(args, "arrival_id", { required: true }), + outbound_date: readStringParam(args, "outbound_date", { required: true }), + return_date: readStringParam(args, "return_date") ?? undefined, + type: readStringParam(args, "type") ?? undefined, + adults: readNumberParam(args, "adults", { integer: true }) ?? undefined, + currency: readStringParam(args, "currency") ?? undefined, + gl: readStringParam(args, "gl") ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/hotels.ts b/src/tools/hotels.ts new file mode 100644 index 0000000..de795b8 --- /dev/null +++ b/src/tools/hotels.ts @@ -0,0 +1,150 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { + jsonResult, + readNumberParam, + readStringParam, +} from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, readBooleanArg, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = [ + "q", + "check_in_date", + "check_out_date", + "adults", + "currency", + "gl", + "hl", + "sort_by", + "min_price", + "max_price", + "hotel_class", + "rating", + "vacation_rentals", + "next_page_token", + "zero_trace", +] as const; + +function isoDateOffset(days: number): string { + const d = new Date(); + d.setUTCDate(d.getUTCDate() + days); + return d.toISOString().slice(0, 10); +} + +function isoDateOffsetFrom(isoDate: string, days: number): string { + const d = new Date(`${isoDate}T00:00:00Z`); + if (isNaN(d.getTime())) return isoDateOffset(days + 1); // fallback: today + days+1 + d.setUTCDate(d.getUTCDate() + days); + return d.toISOString().slice(0, 10); +} + +function parseIsoDate(value: string, label: string): string { + const trimmed = value.trim(); + const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed); + if (!match) { + throw new Error(`serpapi_hotels: ${label} must use YYYY-MM-DD format`); + } + const [, yearStr, monthStr, dayStr] = match; + const year = Number.parseInt(yearStr ?? "", 10); + const month = Number.parseInt(monthStr ?? "", 10); + const day = Number.parseInt(dayStr ?? "", 10); + const d = new Date(Date.UTC(year, month - 1, day)); + if (d.getUTCFullYear() !== year || d.getUTCMonth() !== month - 1 || d.getUTCDate() !== day) { + throw new Error(`serpapi_hotels: ${label} must be a valid calendar date`); + } + return trimmed; +} + +function extract(raw: Record): Record { + return { + engine: "google_hotels", + properties: raw.properties ?? [], + ads: raw.ads ?? [], + }; +} + +export function createSerpApiHotelsTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_hotels", + label: "SerpApi Google Hotels", + description: + "Search hotels and accommodation via Google Hotels. Returns properties with price, rating, amenities, images. " + + "Defaults to tomorrow + 2 nights when dates are not provided.", + parameters: { + type: "object", + properties: { + query: { type: "string", description: "Destination city or area (e.g. 'Paris, France')." }, + check_in_date: { + type: "string", + description: "Check-in date YYYY-MM-DD (default: tomorrow).", + }, + check_out_date: { + type: "string", + description: "Check-out date YYYY-MM-DD (default: check-in + 2 nights).", + }, + adults: { type: "number", description: "Number of adults (default: 1).", minimum: 1 }, + currency: { type: "string", description: "Currency code (e.g. USD, EUR)." }, + gl: { type: "string", description: "Country code (e.g. us, de, ua)." }, + sort_by: { + type: "number", + description: "Sort: 3=lowest price, 8=highest rating, 13=most reviewed.", + enum: [3, 8, 13], + }, + min_price: { type: "number", description: "Minimum price per night." }, + max_price: { type: "number", description: "Maximum price per night." }, + hotel_class: { + type: "string", + description: 'Star class filter, comma-separated (e.g. "4,5" for 4- and 5-star).', + }, + rating: { + type: "number", + description: "Minimum rating: 7=3.5+, 8=4.0+, 9=4.5+.", + enum: [7, 8, 9], + }, + vacation_rentals: { + type: "boolean", + description: "Set true to search vacation rentals instead of hotels.", + }, + next_page_token: { type: "string", description: "Token for next page of results." }, + }, + required: ["query"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const vacationRentals = readBooleanArg(args, "vacation_rentals"); + const rawCheckIn = readStringParam(args, "check_in_date"); + const rawCheckOut = readStringParam(args, "check_out_date"); + const checkIn = rawCheckIn ? parseIsoDate(rawCheckIn, "check_in_date") : isoDateOffset(1); + const checkOut = rawCheckOut + ? parseIsoDate(rawCheckOut, "check_out_date") + : isoDateOffsetFrom(checkIn, 2); + if (rawCheckIn && rawCheckOut && checkOut <= checkIn) { + throw new Error("serpapi_hotels: check_out_date must be after check_in_date"); + } + const raw = await callSerpApi({ + cfg, + engine: "google_hotels", + allowedParams: ALLOWED_PARAMS, + params: { + q: readStringParam(args, "query", { required: true }), + check_in_date: checkIn, + check_out_date: checkOut, + adults: readNumberParam(args, "adults", { integer: true }) ?? undefined, + currency: readStringParam(args, "currency") ?? undefined, + gl: readStringParam(args, "gl") ?? undefined, + sort_by: readNumberParam(args, "sort_by", { integer: true }) ?? undefined, + min_price: readNumberParam(args, "min_price") ?? undefined, + max_price: readNumberParam(args, "max_price") ?? undefined, + hotel_class: readStringParam(args, "hotel_class") ?? undefined, + rating: readNumberParam(args, "rating", { integer: true }) ?? undefined, + vacation_rentals: vacationRentals !== undefined ? String(vacationRentals) : undefined, + next_page_token: readStringParam(args, "next_page_token") ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/immersive-product.ts b/src/tools/immersive-product.ts new file mode 100644 index 0000000..0b89286 --- /dev/null +++ b/src/tools/immersive-product.ts @@ -0,0 +1,64 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { jsonResult, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, readBooleanArg, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = ["page_token", "more_stores", "next_page_token", "zero_trace"] as const; + +function extract(raw: Record): Record { + return { + engine: "google_immersive_product", + product_results: raw.product_results ?? null, + }; +} + +export function createSerpApiImmersiveProductTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_immersive_product", + label: "SerpApi Google Immersive Product", + description: + "Retrieve detailed Google product info for a specific product using a page_token from Google Shopping results. " + + "Returns stores with prices/shipping/discounts, product description, features, user reviews, ratings breakdown, " + + "videos, forum discussions, and similar products. " + + "Get the page_token from serpapi_link in serpapi_shopping results.", + parameters: { + type: "object", + properties: { + page_token: { + type: "string", + description: + "Required token from a previous serpapi_shopping result's serpapi_link (page_token parameter). " + + "Identifies the specific product to look up.", + }, + more_stores: { + type: "boolean", + description: "Fetch up to 13 stores instead of the default 3-5.", + }, + next_page_token: { + type: "string", + description: + "Token from stores_next_page_token in a previous response. Use to retrieve the next page of stores.", + }, + }, + required: ["page_token"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const moreStores = readBooleanArg(args, "more_stores") === true ? "true" : undefined; + const raw = await callSerpApi({ + cfg, + engine: "google_immersive_product", + allowedParams: ALLOWED_PARAMS, + params: { + page_token: readStringParam(args, "page_token", { required: true }), + more_stores: moreStores, + next_page_token: readStringParam(args, "next_page_token") ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/index.ts b/src/tools/index.ts new file mode 100644 index 0000000..7f37518 --- /dev/null +++ b/src/tools/index.ts @@ -0,0 +1,30 @@ +export { createSerpApiAmazonProductTool } from "./amazon-product.js"; +export { createSerpApiAmazonTool } from "./amazon.js"; +export { createSerpApiAutocompleteTool } from "./autocomplete.js"; +export { createSerpApiBingTool } from "./bing.js"; +export { createSerpApiDuckDuckGoTool } from "./duckduckgo.js"; +export { createSerpApiEbayProductTool } from "./ebay-product.js"; +export { createSerpApiEbayTool } from "./ebay.js"; +export { createSerpApiEventsTool } from "./events.js"; +export { createSerpApiFacebookProfileTool } from "./facebook-profile.js"; +export { createSerpApiFinanceTool } from "./finance.js"; +export { createSerpApiFlightsTool } from "./flights.js"; +export { createSerpApiHotelsTool } from "./hotels.js"; +export { createSerpApiImmersiveProductTool } from "./immersive-product.js"; +export { createSerpApiInstagramProfileTool } from "./instagram-profile.js"; +export { createSerpApiJobsTool } from "./jobs.js"; +export { createSerpApiLensTool } from "./lens.js"; +export { createSerpApiMapsReviewsTool } from "./maps-reviews.js"; +export { createSerpApiMapsTool } from "./maps.js"; +export { createSerpApiNewsTool } from "./news.js"; +export { createSerpApiScholarTool } from "./scholar.js"; +export { createSerpApiShoppingTool } from "./shopping.js"; +export { createSerpApiTrendsTool } from "./trends.js"; +export { createSerpApiTripadvisorTool } from "./tripadvisor.js"; +export { createSerpApiWalmartProductTool } from "./walmart-product.js"; +export { createSerpApiWalmartTool } from "./walmart.js"; +export { createSerpApiWeatherTool } from "./weather.js"; +export { createSerpApiYahooTool } from "./yahoo.js"; +export { createSerpApiYouTubeTranscriptTool } from "./youtube-transcript.js"; +export { createSerpApiYouTubeVideoTool } from "./youtube-video.js"; +export { createSerpApiYouTubeTool } from "./youtube.js"; diff --git a/src/tools/instagram-profile.ts b/src/tools/instagram-profile.ts new file mode 100644 index 0000000..7883c62 --- /dev/null +++ b/src/tools/instagram-profile.ts @@ -0,0 +1,62 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { jsonResult, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = ["profile_id", "next_page_token", "zero_trace"] as const; + +function extract(raw: Record): Record { + return { + engine: "instagram_profile", + profile: raw.profile_data ?? raw.profile ?? null, + posts: raw.posts ?? raw.recent_posts ?? [], + reels: raw.reels ?? [], + highlights: raw.highlights ?? [], + related_profiles: raw.related_profiles ?? [], + serpapi_pagination: raw.serpapi_pagination ?? null, + }; +} + +export function createSerpApiInstagramProfileTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_instagram_profile", + label: "SerpApi Instagram Profile", + description: + "Fetch a public Instagram profile via SerpApi. " + + "Returns profile details, posts, reels, highlights, and related profiles. " + + "Use next_page_token from serpapi_pagination to paginate posts.", + parameters: { + type: "object", + properties: { + profile_id: { + type: "string", + description: + "Instagram profile ID from the profile URL. " + + "For https://www.instagram.com/serpapicom, use 'serpapicom'.", + }, + next_page_token: { + type: "string", + description: + "Pagination token from serpapi_pagination.next_page_token returned in a previous response.", + }, + }, + required: ["profile_id"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const raw = await callSerpApi({ + cfg, + engine: "instagram_profile", + allowedParams: ALLOWED_PARAMS, + params: { + profile_id: readStringParam(args, "profile_id", { required: true }), + next_page_token: readStringParam(args, "next_page_token") ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/jobs.ts b/src/tools/jobs.ts new file mode 100644 index 0000000..d4d5370 --- /dev/null +++ b/src/tools/jobs.ts @@ -0,0 +1,84 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { + jsonResult, + readNumberParam, + readStringParam, +} from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = [ + "q", + "gl", + "hl", + "location", + "lrad", + "uds", + "next_page_token", + "zero_trace", +] as const; + +function extract(raw: Record, maxCount: number): Record { + const results = Array.isArray(raw.jobs_results) + ? (raw.jobs_results as unknown[]).slice(0, maxCount) + : []; + return { + engine: "google_jobs", + results, + filters: raw.filters ?? [], + serpapi_pagination: raw.serpapi_pagination ?? null, + }; +} + +export function createSerpApiJobsTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_jobs", + label: "SerpApi Google Jobs", + description: + "Search Google Jobs for job listings. Returns job titles, companies, locations, salary, and description. " + + "Use uds for filtering (values from filters[].parameters.uds in previous response).", + parameters: { + type: "object", + properties: { + query: { + type: "string", + description: 'Job search query, e.g. "software engineer remote".', + }, + count: { + type: "number", + description: "Number of results (1-10).", + minimum: 1, + maximum: 10, + }, + location: { type: "string", description: 'Location string, e.g. "New York, NY".' }, + lrad: { type: "number", description: "Search radius in kilometers." }, + uds: { + type: "string", + description: "Filter string from filters[].parameters.uds in a previous response.", + }, + next_page_token: { type: "string", description: "Token for next page of results." }, + }, + required: ["query"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const count = readNumberParam(args, "count", { integer: true }) ?? 5; + const raw = await callSerpApi({ + cfg, + engine: "google_jobs", + allowedParams: ALLOWED_PARAMS, + params: { + q: readStringParam(args, "query", { required: true }), + location: readStringParam(args, "location") ?? undefined, + lrad: readNumberParam(args, "lrad") ?? undefined, + uds: readStringParam(args, "uds") ?? undefined, + next_page_token: readStringParam(args, "next_page_token") ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw, count)); + }, + }; +} diff --git a/src/tools/lens.ts b/src/tools/lens.ts new file mode 100644 index 0000000..87b3c93 --- /dev/null +++ b/src/tools/lens.ts @@ -0,0 +1,97 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { jsonResult, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, readBooleanArg, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = [ + "url", + "hl", + "country", + "type", + "q", + "safe", + "auto_crop", + "zero_trace", +] as const; + +function extract(raw: Record): Record { + return { + engine: "google_lens", + visual_matches: raw.visual_matches ?? [], + exact_matches: raw.exact_matches ?? [], + related_content: raw.related_content ?? [], + knowledge_graph: raw.knowledge_graph ?? null, + text_results: raw.text_results ?? [], + ai_overview: raw.ai_overview ?? null, + }; +} + +export function createSerpApiLensTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_lens", + label: "SerpApi Google Lens", + description: + "Perform a Google Lens reverse image search via SerpApi. " + + "Provide a public image URL to find visual matches, exact matches, related content, and knowledge graph info. " + + "Use type to narrow results (all/products/visual_matches/exact_matches/about_this_image).", + parameters: { + type: "object", + properties: { + url: { + type: "string", + description: "Public URL of the image to search with Google Lens.", + }, + type: { + type: "string", + enum: ["all", "about_this_image", "products", "exact_matches", "visual_matches"], + description: "Type of search to perform (default: all).", + }, + q: { + type: "string", + description: + "Optional search query to refine results. Only applicable when type is all, visual_matches, or products.", + }, + hl: { + type: "string", + description: "Two-letter language code (e.g. en, de, fr).", + }, + country: { + type: "string", + description: "Two-letter country code (e.g. us, fr, de).", + }, + safe: { + type: "string", + enum: ["active", "off"], + description: "Filter adult content. active = filter, off = show.", + }, + auto_crop: { + type: "boolean", + description: + "Whether Google auto-crops the image to focus on the detected area of interest (default: false). Not applicable for type=about_this_image.", + }, + }, + required: ["url"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const raw = await callSerpApi({ + cfg, + engine: "google_lens", + allowedParams: ALLOWED_PARAMS, + params: { + url: readStringParam(args, "url", { required: true }), + type: readStringParam(args, "type") ?? undefined, + q: readStringParam(args, "q") ?? undefined, + hl: readStringParam(args, "hl") ?? undefined, + country: readStringParam(args, "country") ?? undefined, + safe: readStringParam(args, "safe") ?? undefined, + auto_crop: readBooleanArg(args, "auto_crop") === true ? "true" : undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/maps-reviews.ts b/src/tools/maps-reviews.ts new file mode 100644 index 0000000..45b6ed6 --- /dev/null +++ b/src/tools/maps-reviews.ts @@ -0,0 +1,143 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { + jsonResult, + readNumberParam, + readStringParam, + wrapWebContent, +} from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = [ + "data_id", + "place_id", + "hl", + "sort_by", + "topic_id", + "query", + "num", + "next_page_token", + "zero_trace", +] as const; + +function extract(raw: Record): Record { + const reviews = Array.isArray(raw.reviews) + ? (raw.reviews as Record[]).map((r) => ({ + position: r.position, + rating: r.rating, + date: r.date, + iso_date: r.iso_date, + snippet: typeof r.snippet === "string" ? wrapWebContent(r.snippet) : (r.snippet ?? null), + user: r.user ?? null, + details: r.details ?? null, + likes: r.likes, + images: r.images ?? [], + })) + : []; + return { + engine: "google_maps_reviews", + place_info: raw.place_info ?? null, + topics: raw.topics ?? [], + reviews, + serpapi_pagination: raw.serpapi_pagination ?? null, + }; +} + +export function createSerpApiMapsReviewsTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_maps_reviews", + label: "SerpApi Google Maps Reviews", + description: + "Fetch reviews for a place on Google Maps via SerpApi. " + + "Requires either data_id (from serpapi_maps results) or place_id. " + + "Returns place info, review topics, and individual reviews with ratings, snippets, and user details.", + parameters: { + type: "object", + properties: { + data_id: { + type: "string", + description: + "Google Maps data ID for the place. Obtain from serpapi_maps results. Either data_id or place_id is required.", + }, + place_id: { + type: "string", + description: "Google Maps place ID. Either place_id or data_id is required.", + }, + hl: { + type: "string", + description: "Two-letter language code for results (e.g. en, de, fr). Defaults to en.", + }, + sort_by: { + type: "string", + enum: ["qualityScore", "newestFirst", "ratingHigh", "ratingLow"], + description: + "Sort order for reviews. qualityScore = most relevant (default), newestFirst = most recent, ratingHigh/ratingLow = by rating.", + }, + topic_id: { + type: "string", + description: + "Filter reviews by topic ID (from the topics array in the response). Cannot be used with query.", + }, + query: { + type: "string", + description: "Text query to filter reviews. Cannot be used with topic_id.", + }, + num: { + type: "number", + description: + "Maximum number of reviews to return (1–20, default 10). Cannot be used on the initial page without next_page_token/topic_id/query.", + minimum: 1, + maximum: 20, + }, + next_page_token: { + type: "string", + description: + "Pagination token from serpapi_pagination.next_page_token to fetch the next page.", + }, + }, + required: [], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const dataId = readStringParam(args, "data_id"); + const placeId = readStringParam(args, "place_id"); + if (!dataId && !placeId) { + throw new Error("serpapi_maps_reviews: either data_id or place_id is required"); + } + if (dataId && placeId) { + throw new Error("serpapi_maps_reviews: provide either data_id or place_id, not both"); + } + const topicId = readStringParam(args, "topic_id"); + const query = readStringParam(args, "query"); + if (topicId && query) { + throw new Error("serpapi_maps_reviews: topic_id and query are mutually exclusive"); + } + const nextPageToken = readStringParam(args, "next_page_token"); + const num = readNumberParam(args, "num", { integer: true }) ?? undefined; + if (num !== undefined && !nextPageToken && !topicId && !query) { + throw new Error( + "serpapi_maps_reviews: num cannot be used on the initial page without next_page_token, topic_id, or query", + ); + } + const raw = await callSerpApi({ + cfg, + engine: "google_maps_reviews", + allowedParams: ALLOWED_PARAMS, + params: { + data_id: dataId ?? undefined, + place_id: placeId ?? undefined, + hl: readStringParam(args, "hl") ?? undefined, + sort_by: readStringParam(args, "sort_by") ?? undefined, + topic_id: topicId ?? undefined, + query: query ?? undefined, + num, + next_page_token: nextPageToken ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/maps.ts b/src/tools/maps.ts new file mode 100644 index 0000000..d45c341 --- /dev/null +++ b/src/tools/maps.ts @@ -0,0 +1,90 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { + jsonResult, + readNumberParam, + readStringParam, +} from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = [ + "q", + "gl", + "hl", + "ll", + "location", + "type", + "nearby", + "start", + "zero_trace", +] as const; + +function extract(raw: Record, maxCount: number): Record { + const results = Array.isArray(raw.local_results) + ? (raw.local_results as unknown[]).slice(0, maxCount) + : []; + return { + engine: "google_maps", + results, + serpapi_pagination: raw.serpapi_pagination ?? null, + }; +} + +export function createSerpApiMapsTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_maps", + label: "SerpApi Google Maps", + description: + "Find local businesses, restaurants, services, and points of interest via Google Maps. " + + "Returns name, address, rating, reviews, hours. Use ll for GPS precision: @lat,lng,zoom.", + parameters: { + type: "object", + properties: { + query: { type: "string", description: "Place or business type query." }, + ll: { + type: "string", + description: 'GPS coordinates @lat,lng,zoom (e.g. "@40.7128,-74.006,14z").', + }, + location: { type: "string", description: "City or area string (e.g. 'Austin, Texas')." }, + count: { + type: "number", + description: "Number of results (1-20).", + minimum: 1, + maximum: 20, + }, + gl: { type: "string", description: "Country code (e.g. us, de, ua)." }, + nearby: { + type: "string", + description: + "Force results near this location. Recommended when query contains 'near me'.", + }, + start: { type: "number", description: "Result offset for pagination (0, 20, 40...)." }, + }, + required: ["query"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const count = readNumberParam(args, "count", { integer: true }) ?? 5; + const ll = readStringParam(args, "ll"); + const location = readStringParam(args, "location"); + const raw = await callSerpApi({ + cfg, + engine: "google_maps", + allowedParams: ALLOWED_PARAMS, + params: { + q: readStringParam(args, "query", { required: true }), + type: "search", + ll: ll ?? undefined, + location: location ?? undefined, + gl: readStringParam(args, "gl") ?? undefined, + nearby: readStringParam(args, "nearby") ?? undefined, + start: readNumberParam(args, "start", { integer: true }) ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw, count)); + }, + }; +} diff --git a/src/tools/news.ts b/src/tools/news.ts new file mode 100644 index 0000000..f2be5b6 --- /dev/null +++ b/src/tools/news.ts @@ -0,0 +1,116 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { + jsonResult, + readNumberParam, + readStringParam, +} from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = [ + "q", + "gl", + "hl", + "topic_token", + "publication_token", + "section_token", + "story_token", + "so", + "zero_trace", +] as const; + +function extract(raw: Record, maxCount: number): Record { + const results = Array.isArray(raw.news_results) + ? (raw.news_results as unknown[]).slice(0, maxCount) + : []; + return { + engine: "google_news", + results, + menu_links: raw.menu_links ?? [], + related_topics: raw.related_topics ?? [], + related_publications: raw.related_publications ?? [], + }; +} + +export function createSerpApiNewsTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_news", + label: "SerpApi Google News", + description: + "Search Google News for recent articles. Returns headlines, sources, dates, and URLs. " + + "Use topic_token/publication_token for browsing topics or publishers. " + + "so: 0=relevance (default), 1=date. Time filter via q: e.g. q='coffee when:1d'.", + parameters: { + type: "object", + properties: { + query: { + type: "string", + description: "News search query. Use 'when:1d' for time filter, e.g. 'AI when:7d'.", + }, + count: { + type: "number", + description: "Number of results (1-10).", + minimum: 1, + maximum: 10, + }, + gl: { type: "string", description: "Country code (e.g. us, de, ua)." }, + hl: { type: "string", description: "Language code override (e.g. en, de, uk)." }, + so: { + type: "number", + description: "Sort: 0=relevance (default), 1=date.", + enum: [0, 1], + }, + topic_token: { + type: "string", + description: "Google News topic token (from menu_links[].topic_token).", + }, + publication_token: { + type: "string", + description: "Publisher token (from related_publications[].publication_token).", + }, + section_token: { + type: "string", + description: "Sub-section token. Use with topic_token or publication_token.", + }, + story_token: { + type: "string", + description: "Story token for full coverage of a specific story.", + }, + }, + required: [], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const query = readStringParam(args, "query"); + const topicToken = readStringParam(args, "topic_token"); + const publicationToken = readStringParam(args, "publication_token"); + const sectionToken = readStringParam(args, "section_token"); + const storyToken = readStringParam(args, "story_token"); + if (!query && !topicToken && !publicationToken && !sectionToken && !storyToken) { + throw new Error( + "serpapi_news: at least one of query, topic_token, publication_token, section_token, or story_token is required", + ); + } + const cfg = resolveToolConfig(api, ctx); + const count = readNumberParam(args, "count", { integer: true }) ?? 5; + const raw = await callSerpApi({ + cfg, + engine: "google_news", + allowedParams: ALLOWED_PARAMS, + params: { + q: query ?? undefined, + gl: readStringParam(args, "gl") ?? undefined, + hl: readStringParam(args, "hl") ?? undefined, + so: readNumberParam(args, "so", { integer: true }) ?? undefined, + topic_token: topicToken ?? undefined, + publication_token: publicationToken ?? undefined, + section_token: sectionToken ?? undefined, + story_token: storyToken ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw, count)); + }, + }; +} diff --git a/src/tools/scholar.ts b/src/tools/scholar.ts new file mode 100644 index 0000000..fc3ca8c --- /dev/null +++ b/src/tools/scholar.ts @@ -0,0 +1,119 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { + jsonResult, + readNumberParam, + readStringParam, +} from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = [ + "q", + "hl", + "lr", + "as_ylo", + "as_yhi", + "scisbd", + "cites", + "cluster", + "as_sdt", + "start", + "num", + "zero_trace", +] as const; + +function extract(raw: Record, maxCount: number): Record { + const results = Array.isArray(raw.organic_results) + ? (raw.organic_results as unknown[]).slice(0, maxCount) + : []; + return { + engine: "google_scholar", + results, + related_searches: raw.related_searches ?? [], + }; +} + +export function createSerpApiScholarTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_scholar", + label: "SerpApi Google Scholar", + description: + "Search Google Scholar for academic papers, research articles, and citations. " + + "Use cites= for Cited By search, cluster= for All Versions.", + parameters: { + type: "object", + properties: { + query: { + type: "string", + description: "Academic search query. Optional when using cites or cluster.", + }, + count: { + type: "number", + description: "Number of results (1-20).", + minimum: 1, + maximum: 20, + }, + as_ylo: { type: "number", description: "Filter from this year (e.g. 2020)." }, + as_yhi: { type: "number", description: "Filter until this year." }, + scisbd: { + type: "number", + description: "Sort by date: 1=date, 0=relevance (default).", + enum: [0, 1], + }, + cites: { + type: "string", + description: "Article ID to find citing papers (Cited By search).", + }, + cluster: { + type: "string", + description: "Article ID to find all versions. Cannot use with q+cites.", + }, + as_sdt: { + type: "string", + description: + "Search type: 0=exclude patents (default), 7=include patents, 4=case law (US).", + }, + lr: { + type: "string", + description: 'Language restriction, pipe-separated (e.g. "lang_en|lang_de").', + }, + start: { type: "number", description: "Result offset for pagination (0, 10, 20...)." }, + }, + required: [], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const query = readStringParam(args, "query"); + const cites = readStringParam(args, "cites"); + const cluster = readStringParam(args, "cluster"); + if (!query && !cites && !cluster) { + throw new Error("serpapi_scholar: at least one of query, cites, or cluster is required"); + } + if (cluster && (query || cites)) { + throw new Error("serpapi_scholar: cluster cannot be used together with query or cites"); + } + const count = readNumberParam(args, "count", { integer: true }) ?? 5; + const raw = await callSerpApi({ + cfg, + engine: "google_scholar", + allowedParams: ALLOWED_PARAMS, + params: { + q: query, + as_ylo: readNumberParam(args, "as_ylo", { integer: true }) ?? undefined, + as_yhi: readNumberParam(args, "as_yhi", { integer: true }) ?? undefined, + scisbd: readNumberParam(args, "scisbd", { integer: true }) ?? undefined, + cites: cites ?? undefined, + cluster: cluster ?? undefined, + as_sdt: readStringParam(args, "as_sdt") ?? undefined, + lr: readStringParam(args, "lr") ?? undefined, + start: readNumberParam(args, "start", { integer: true }) ?? undefined, + num: count, + }, + signal, + }); + return jsonResult(extract(raw, count)); + }, + }; +} diff --git a/src/tools/shopping.ts b/src/tools/shopping.ts new file mode 100644 index 0000000..d3847cd --- /dev/null +++ b/src/tools/shopping.ts @@ -0,0 +1,98 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { + jsonResult, + readNumberParam, + readStringParam, +} from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, readBooleanArg, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = [ + "q", + "gl", + "hl", + "min_price", + "max_price", + "sort_by", + "free_shipping", + "on_sale", + "shoprs", + "start", + "zero_trace", +] as const; + +function extract(raw: Record, maxCount: number): Record { + const results = Array.isArray(raw.shopping_results) + ? (raw.shopping_results as unknown[]).slice(0, maxCount) + : []; + return { + engine: "google_shopping", + results, + filters: raw.filters ?? [], + serpapi_pagination: raw.serpapi_pagination ?? null, + }; +} + +export function createSerpApiShoppingTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_shopping", + label: "SerpApi Google Shopping", + description: + "Search Google Shopping for products. Returns product names, prices, stores, and ratings. " + + "Best for comparing prices across retailers.", + parameters: { + type: "object", + properties: { + query: { type: "string", description: "Product search query." }, + count: { + type: "number", + description: "Number of results (1-20).", + minimum: 1, + maximum: 20, + }, + gl: { type: "string", description: "Country code (e.g. us, de, ua)." }, + min_price: { type: "number", description: "Minimum price filter." }, + max_price: { type: "number", description: "Maximum price filter." }, + sort_by: { + type: "number", + description: "Sort: 1=price low-to-high, 2=price high-to-low.", + enum: [1, 2], + }, + free_shipping: { type: "boolean", description: "Show only products with free shipping." }, + on_sale: { type: "boolean", description: "Show only products on sale." }, + shoprs: { + type: "string", + description: "Filter token from filters[].options[].shoprs in a previous response.", + }, + start: { type: "number", description: "Result offset for pagination (0, 60, 120...)." }, + }, + required: ["query"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const count = readNumberParam(args, "count", { integer: true }) ?? 5; + const freeShipping = readBooleanArg(args, "free_shipping"); + const onSale = readBooleanArg(args, "on_sale"); + const raw = await callSerpApi({ + cfg, + engine: "google_shopping", + allowedParams: ALLOWED_PARAMS, + params: { + q: readStringParam(args, "query", { required: true }), + gl: readStringParam(args, "gl") ?? undefined, + min_price: readNumberParam(args, "min_price") ?? undefined, + max_price: readNumberParam(args, "max_price") ?? undefined, + sort_by: readNumberParam(args, "sort_by", { integer: true }) ?? undefined, + free_shipping: freeShipping !== undefined ? String(freeShipping) : undefined, + on_sale: onSale !== undefined ? String(onSale) : undefined, + shoprs: readStringParam(args, "shoprs") ?? undefined, + start: readNumberParam(args, "start", { integer: true }) ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw, count)); + }, + }; +} diff --git a/src/tools/trends.ts b/src/tools/trends.ts new file mode 100644 index 0000000..cbcf38c --- /dev/null +++ b/src/tools/trends.ts @@ -0,0 +1,105 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { + jsonResult, + readNumberParam, + readStringParam, +} from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = [ + "q", + "geo", + "date", + "data_type", + "hl", + "cat", + "gprop", + "region", + "tz", + "zero_trace", +] as const; + +function extract(raw: Record): Record { + return { + engine: "google_trends", + interest_over_time: raw.interest_over_time ?? null, + interest_by_region: raw.interest_by_region ?? null, + related_topics: raw.related_topics ?? null, + related_queries: raw.related_queries ?? null, + }; +} + +export function createSerpApiTrendsTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_trends", + label: "SerpApi Google Trends", + description: + "Get Google Trends data: interest over time, by region, related topics, or related queries. " + + "data_type: TIMESERIES (default), GEO_MAP, GEO_MAP_0, RELATED_TOPICS, RELATED_QUERIES.", + parameters: { + type: "object", + properties: { + query: { + type: "string", + description: + 'Search term(s). Up to 5 comma-separated for TIMESERIES/GEO_MAP (e.g. "coffee,tea,juice").', + }, + geo: { type: "string", description: "Region code (e.g. US, DE, UA). Omit for worldwide." }, + date: { + type: "string", + description: + 'Date range: "now 1-d", "now 7-d", "today 1-m", "today 12-m", "today 5-y" (default: "today 12-m").', + }, + data_type: { + type: "string", + enum: ["TIMESERIES", "GEO_MAP", "GEO_MAP_0", "RELATED_TOPICS", "RELATED_QUERIES"], + description: 'Trend data type (default: "TIMESERIES").', + }, + region: { + type: "string", + enum: ["COUNTRY", "REGION", "DMA", "CITY"], + description: "Region breakdown level for GEO_MAP/GEO_MAP_0 data types.", + }, + cat: { + type: "string", + description: "Search category ID (default: 0 = all categories).", + }, + gprop: { + type: "string", + enum: ["images", "news", "froogle", "youtube"], + description: + "Property filter: images, news, froogle=Shopping, youtube. Omit for Web (default).", + }, + tz: { + type: "number", + description: + "Timezone offset in minutes (e.g. -540 for Tokyo, 420 for PDT). Default: 420.", + }, + }, + required: ["query"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const raw = await callSerpApi({ + cfg, + engine: "google_trends", + allowedParams: ALLOWED_PARAMS, + params: { + q: readStringParam(args, "query", { required: true }), + data_type: readStringParam(args, "data_type") ?? "TIMESERIES", + date: readStringParam(args, "date") ?? "today 12-m", + geo: readStringParam(args, "geo") ?? undefined, + region: readStringParam(args, "region") ?? undefined, + cat: readStringParam(args, "cat") ?? undefined, + gprop: readStringParam(args, "gprop") ?? undefined, + tz: readNumberParam(args, "tz", { integer: true }) ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/tripadvisor.ts b/src/tools/tripadvisor.ts new file mode 100644 index 0000000..9e38f3f --- /dev/null +++ b/src/tools/tripadvisor.ts @@ -0,0 +1,102 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { + jsonResult, + readNumberParam, + readStringParam, +} from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = [ + "q", + "lat", + "lon", + "tripadvisor_domain", + "ssrc", + "offset", + "limit", + "zero_trace", +] as const; + +function extract(raw: Record): Record { + return { + engine: "tripadvisor", + places: raw.places ?? [], + restaurants: raw.restaurants ?? [], + hotels: raw.hotels ?? [], + attractions: raw.attractions ?? [], + serpapi_pagination: raw.serpapi_pagination ?? null, + }; +} + +export function createSerpApiTripadvisorTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_tripadvisor", + label: "SerpApi Tripadvisor Search", + description: + "Search Tripadvisor for destinations, hotels, restaurants, and attractions via SerpApi. " + + "Use ssrc to filter by category. Returns place names, descriptions, ratings, and links.", + parameters: { + type: "object", + properties: { + query: { + type: "string", + description: "Search query (e.g. 'Rome', 'best restaurants in Paris', 'hotels in Kyiv').", + }, + ssrc: { + type: "string", + enum: ["a", "r", "A", "h", "g", "f"], + description: + "Filter results by category: a = All (default), r = Restaurants, A = Things to Do, h = Hotels, g = Destinations, f = Forums.", + }, + tripadvisor_domain: { + type: "string", + description: + "Tripadvisor domain to use (e.g. 'www.tripadvisor.co.uk'). Defaults to tripadvisor.com.", + }, + lat: { + type: "number", + description: "GPS latitude for location-based search origin.", + }, + lon: { + type: "number", + description: "GPS longitude for location-based search origin.", + }, + limit: { + type: "number", + description: "Maximum number of results to return (default: 30, max: 100).", + minimum: 1, + maximum: 100, + }, + offset: { + type: "number", + description: + "Result offset for pagination (default: 0; use 30 for page 2, 60 for page 3, ...).", + minimum: 0, + }, + }, + required: ["query"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const raw = await callSerpApi({ + cfg, + engine: "tripadvisor", + allowedParams: ALLOWED_PARAMS, + params: { + q: readStringParam(args, "query", { required: true }), + ssrc: readStringParam(args, "ssrc") ?? undefined, + tripadvisor_domain: readStringParam(args, "tripadvisor_domain") ?? undefined, + lat: readNumberParam(args, "lat") ?? undefined, + lon: readNumberParam(args, "lon") ?? undefined, + limit: readNumberParam(args, "limit", { integer: true }) ?? undefined, + offset: readNumberParam(args, "offset", { integer: true }) ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/walmart-product.ts b/src/tools/walmart-product.ts new file mode 100644 index 0000000..d0046da --- /dev/null +++ b/src/tools/walmart-product.ts @@ -0,0 +1,56 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { jsonResult, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = ["product_id", "store_id", "zero_trace"] as const; + +function extract(raw: Record): Record { + return { + engine: "walmart_product", + product_result: raw.product_result ?? null, + reviews_results: raw.reviews_results ?? null, + }; +} + +export function createSerpApiWalmartProductTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_walmart_product", + label: "SerpApi Walmart Product", + description: + "Fetch detailed product information from a Walmart listing by product ID via SerpApi. " + + "Returns title, price, description, specs, stock status, shipping/pickup options, and reviews. " + + "Use serpapi_walmart to search for products and get product IDs first.", + parameters: { + type: "object", + properties: { + product_id: { + type: "string", + description: + "Walmart product ID or us_item_id. Found in the listing URL (e.g. '138762768' from walmart.com/ip/name/138762768).", + }, + store_id: { + type: "string", + description: "Store ID to get pricing for a specific Walmart store location.", + }, + }, + required: ["product_id"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const raw = await callSerpApi({ + cfg, + engine: "walmart_product", + allowedParams: ALLOWED_PARAMS, + params: { + product_id: readStringParam(args, "product_id", { required: true }), + store_id: readStringParam(args, "store_id") ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/walmart.ts b/src/tools/walmart.ts new file mode 100644 index 0000000..2071f90 --- /dev/null +++ b/src/tools/walmart.ts @@ -0,0 +1,118 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { + jsonResult, + readNumberParam, + readStringParam, +} from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = [ + "query", + "walmart_domain", + "sort", + "cat_id", + "facet", + "store_id", + "min_price", + "max_price", + "page", + "zero_trace", +] as const; + +function extract(raw: Record): Record { + return { + engine: "walmart", + search_information: raw.search_information ?? null, + organic_results: raw.organic_results ?? [], + serpapi_pagination: raw.serpapi_pagination ?? null, + }; +} + +export function createSerpApiWalmartTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_walmart", + label: "SerpApi Walmart Search", + description: + "Search Walmart product listings via SerpApi. Returns product titles, prices, ratings, and links. " + + "Supports filtering by price range, category, and sort order. " + + "Use serpapi_amazon or serpapi_ebay for those marketplaces.", + parameters: { + type: "object", + properties: { + query: { + type: "string", + description: "Walmart search query. Optional when cat_id is specified.", + }, + walmart_domain: { + type: "string", + description: + "Walmart domain to use (e.g. walmart.ca, walmart.com.mx). Defaults to walmart.com.", + }, + sort: { + type: "string", + enum: ["price_low", "price_high", "best_seller", "best_match", "rating_high", "new"], + description: "Sort order for results.", + }, + cat_id: { + type: "string", + description: + "Category ID to narrow search (e.g. '0' for all departments). " + + "Obtain from a previous results' categories. Either query or cat_id is required.", + }, + facet: { + type: "string", + description: + "Attribute filters as key:value pairs separated by || (e.g. 'brand:Apple||color:Red').", + }, + store_id: { + type: "string", + description: "Store ID to filter products by a specific Walmart store.", + }, + min_price: { + type: "number", + description: "Minimum price filter.", + }, + max_price: { + type: "number", + description: "Maximum price filter.", + }, + page: { + type: "number", + description: "Page number for pagination (default: 1, max: 100).", + minimum: 1, + maximum: 100, + }, + }, + required: [], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const query = readStringParam(args, "query"); + const catId = readStringParam(args, "cat_id"); + if (!query && !catId) { + throw new Error("serpapi_walmart: either query or cat_id is required"); + } + const raw = await callSerpApi({ + cfg, + engine: "walmart", + allowedParams: ALLOWED_PARAMS, + params: { + query: query ?? undefined, + walmart_domain: readStringParam(args, "walmart_domain") ?? undefined, + sort: readStringParam(args, "sort") ?? undefined, + cat_id: catId ?? undefined, + facet: readStringParam(args, "facet") ?? undefined, + store_id: readStringParam(args, "store_id") ?? undefined, + min_price: readNumberParam(args, "min_price") ?? undefined, + max_price: readNumberParam(args, "max_price") ?? undefined, + page: readNumberParam(args, "page", { integer: true }) ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/weather.ts b/src/tools/weather.ts new file mode 100644 index 0000000..6a45ccc --- /dev/null +++ b/src/tools/weather.ts @@ -0,0 +1,61 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { jsonResult, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = ["q", "gl", "hl", "google_domain", "zero_trace"] as const; + +function extract(raw: Record): Record { + const box = (raw.answer_box ?? null) as Record | null; + return { + engine: "google", + answer_box: box, + }; +} + +export function createSerpApiWeatherTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_weather", + label: "SerpApi Weather", + description: + "Get current weather or forecast for a location via Google (SerpApi). " + + "Returns temperature (high/low), conditions, date, and location from Google's weather answer box. " + + "Ask naturally, e.g. 'weather in Kyiv tomorrow' or 'weather forecast for Paris this week'.", + parameters: { + type: "object", + properties: { + query: { + type: "string", + description: + "Natural language weather query (e.g. 'weather in Kyiv', 'weather tomorrow in Berlin', 'forecast for Tokyo this week').", + }, + gl: { + type: "string", + description: "Two-letter country code for localized results (e.g. us, gb, ua).", + }, + hl: { + type: "string", + description: "Two-letter language code for the response (e.g. en, de, uk).", + }, + }, + required: ["query"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const raw = await callSerpApi({ + cfg, + engine: "google", + allowedParams: ALLOWED_PARAMS, + params: { + q: readStringParam(args, "query", { required: true }), + gl: readStringParam(args, "gl") ?? undefined, + hl: readStringParam(args, "hl") ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/yahoo.ts b/src/tools/yahoo.ts new file mode 100644 index 0000000..e8afc35 --- /dev/null +++ b/src/tools/yahoo.ts @@ -0,0 +1,115 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { + jsonResult, + readNumberParam, + readStringParam, + wrapWebContent, +} from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = [ + "p", + "yahoo_domain", + "vc", + "vl", + "b", + "vm", + "vs", + "vf", + "zero_trace", +] as const; + +function extract(raw: Record): Record { + const organicResults = Array.isArray(raw.organic_results) + ? (raw.organic_results as Record[]).map((r) => ({ + position: r.position, + title: typeof r.title === "string" ? wrapWebContent(r.title) : (r.title ?? null), + url: r.link ?? null, + displayed_link: r.displayed_link ?? null, + snippet: typeof r.snippet === "string" ? wrapWebContent(r.snippet) : (r.snippet ?? null), + })) + : []; + return { + engine: "yahoo", + results: organicResults, + related_searches: raw.related_searches ?? null, + serpapi_pagination: raw.serpapi_pagination ?? null, + }; +} + +export function createSerpApiYahooTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_yahoo", + label: "SerpApi Yahoo! Search", + description: + "Search the web using Yahoo! via SerpApi. Returns titles, URLs, and snippets. " + + "Supports Yahoo operators and domain/language filtering. " + + "Useful as an alternative to Google or Bing results.", + parameters: { + type: "object", + properties: { + query: { + type: "string", + description: "Search query string.", + }, + yahoo_domain: { + type: "string", + description: + "Yahoo! domain to use (e.g. 'fr' for fr.search.yahoo.com). Defaults to search.yahoo.com.", + }, + vc: { + type: "string", + description: "Two-letter country code to target (e.g. us, gb, fr).", + }, + vl: { + type: "string", + description: + "Language filter in the format lang_{code} (e.g. lang_fr to search French only).", + }, + vm: { + type: "string", + enum: ["r", "i", "p"], + description: "Adult content filter: r = Strict, i = Moderate, p = Off.", + }, + vs: { + type: "string", + description: "Filter results by top-level domains, comma-separated (e.g. .com,.org).", + }, + vf: { + type: "string", + description: "File format filter (e.g. pdf, txt) or 'all formats'.", + }, + b: { + type: "number", + description: + "Result offset for pagination (default: 1; use 11 for page 2, 21 for page 3, ...).", + minimum: 1, + }, + }, + required: ["query"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const raw = await callSerpApi({ + cfg, + engine: "yahoo", + allowedParams: ALLOWED_PARAMS, + params: { + p: readStringParam(args, "query", { required: true }), + yahoo_domain: readStringParam(args, "yahoo_domain") ?? undefined, + vc: readStringParam(args, "vc") ?? undefined, + vl: readStringParam(args, "vl") ?? undefined, + vm: readStringParam(args, "vm") ?? undefined, + vs: readStringParam(args, "vs") ?? undefined, + vf: readStringParam(args, "vf") ?? undefined, + b: readNumberParam(args, "b", { integer: true }) ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/youtube-transcript.ts b/src/tools/youtube-transcript.ts new file mode 100644 index 0000000..a3e4b92 --- /dev/null +++ b/src/tools/youtube-transcript.ts @@ -0,0 +1,70 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { jsonResult, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = ["v", "language_code", "title", "type", "zero_trace"] as const; + +function extract(raw: Record): Record { + return { + engine: "youtube_video_transcript", + video_id: raw.video_id ?? null, + title: raw.title ?? null, + language_code: raw.language_code ?? null, + transcript: raw.transcript ?? [], + }; +} + +export function createSerpApiYouTubeTranscriptTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_youtube_transcript", + label: "SerpApi YouTube Video Transcript", + description: + "Fetch the transcript of a YouTube video via SerpApi. " + + "Returns timestamped transcript segments. " + + "Use language_code to request a specific language; falls back to the first available if not found.", + parameters: { + type: "object", + properties: { + v: { + type: "string", + description: + "YouTube video ID from the URL (e.g. 'dQw4w9WgXcQ' from youtube.com/watch?v=dQw4w9WgXcQ or youtu.be/dQw4w9WgXcQ).", + }, + language_code: { + type: "string", + description: + "Language code for the transcript (e.g. en, es-ES, zh-Hans). Defaults to en. Falls back to first available if the requested language is unavailable.", + }, + title: { + type: "string", + description: "Specific transcript title to select (e.g. 'Twitch Chat - Simple').", + }, + type: { + type: "string", + description: + "Transcript type filter. Use 'asr' for auto-generated (automatic speech recognition) transcripts.", + }, + }, + required: ["v"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const raw = await callSerpApi({ + cfg, + engine: "youtube_video_transcript", + allowedParams: ALLOWED_PARAMS, + params: { + v: readStringParam(args, "v", { required: true }), + language_code: readStringParam(args, "language_code") ?? undefined, + title: readStringParam(args, "title") ?? undefined, + type: readStringParam(args, "type") ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/youtube-video.ts b/src/tools/youtube-video.ts new file mode 100644 index 0000000..8c2fa81 --- /dev/null +++ b/src/tools/youtube-video.ts @@ -0,0 +1,80 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { jsonResult, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = ["v", "gl", "hl", "next_page_token", "zero_trace"] as const; + +function extract(raw: Record): Record { + return { + engine: "youtube_video", + title: raw.title ?? null, + thumbnail: raw.thumbnail ?? null, + channel: raw.channel ?? null, + views: raw.extracted_views ?? null, + likes: raw.extracted_likes ?? null, + published_date: raw.published_date ?? null, + description: raw.description ?? null, + chapters: raw.chapters ?? [], + related_videos: raw.related_videos ?? [], + related_videos_next_page_token: raw.related_videos_next_page_token ?? null, + end_screen_videos: raw.end_screen_videos ?? [], + comments_next_page_token: raw.comments_next_page_token ?? null, + comments_sorting_token: raw.comments_sorting_token ?? [], + transcript: raw.transcript ?? null, + }; +} + +export function createSerpApiYouTubeVideoTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_youtube_video", + label: "SerpApi YouTube Video", + description: + "Fetch metadata for a YouTube video via SerpApi: title, description, channel, views, likes, chapters, related videos, and comment pagination tokens. " + + "Use next_page_token to paginate related videos or comments. " + + "Use serpapi_youtube_transcript to get the full transcript.", + parameters: { + type: "object", + properties: { + v: { + type: "string", + description: + "YouTube video ID (e.g. 'dQw4w9WgXcQ' from youtube.com/watch?v=dQw4w9WgXcQ).", + }, + hl: { + type: "string", + description: "Two-letter language code for the response (e.g. en, de, fr).", + }, + gl: { + type: "string", + description: "Two-letter country code (e.g. us, gb, fr).", + }, + next_page_token: { + type: "string", + description: + "Pagination token for related videos, comments, or replies. " + + "Use related_videos_next_page_token, comments_next_page_token, comments_sorting_token[].token, or replies_next_page_token from a previous response.", + }, + }, + required: ["v"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const raw = await callSerpApi({ + cfg, + engine: "youtube_video", + allowedParams: ALLOWED_PARAMS, + params: { + v: readStringParam(args, "v", { required: true }), + hl: readStringParam(args, "hl") ?? undefined, + gl: readStringParam(args, "gl") ?? undefined, + next_page_token: readStringParam(args, "next_page_token") ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/tools/youtube.ts b/src/tools/youtube.ts new file mode 100644 index 0000000..81882bc --- /dev/null +++ b/src/tools/youtube.ts @@ -0,0 +1,52 @@ +import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { jsonResult, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; +import { callSerpApi } from "../serpapi-client.js"; +import { type SerpApiToolCtx, resolveToolConfig } from "../utils.js"; + +const ALLOWED_PARAMS = ["search_query", "hl", "sp", "zero_trace"] as const; + +function extract(raw: Record): Record { + return { + engine: "youtube", + channel_results: raw.channel_results ?? [], + video_results: raw.video_results ?? [], + shorts_results: raw.shorts_results ?? [], + }; +} + +export function createSerpApiYouTubeTool(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): AnyAgentTool { + return { + name: "serpapi_youtube", + label: "SerpApi YouTube Search", + description: + "Search YouTube for videos, channels, and shorts. Returns video_results (title, views, duration, channel), " + + "channel_results (name, subscribers, handle), and shorts_results.", + parameters: { + type: "object", + properties: { + query: { type: "string", description: "YouTube search query." }, + sp: { + type: "string", + description: 'YouTube search filters (e.g. "EgQIBBAB" for this week). Advanced use only.', + }, + }, + required: ["query"], + additionalProperties: false, + }, + execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { + const cfg = resolveToolConfig(api, ctx); + const raw = await callSerpApi({ + cfg, + engine: "youtube", + allowedParams: ALLOWED_PARAMS, + params: { + search_query: readStringParam(args, "query", { required: true }), + sp: readStringParam(args, "sp") ?? undefined, + }, + signal, + }); + return jsonResult(extract(raw)); + }, + }; +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..7255ec5 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,20 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; + +export type SerpApiToolCtx = Pick< + OpenClawPluginToolContext, + "config" | "runtimeConfig" | "getRuntimeConfig" +>; + +export function resolveToolConfig(api: OpenClawPluginApi, ctx?: SerpApiToolCtx): OpenClawConfig { + return ctx?.getRuntimeConfig?.() ?? ctx?.runtimeConfig ?? ctx?.config ?? api.config; +} + +/** Reads a boolean tool argument that may arrive as a real boolean or as "true"/"false" string. */ +export function readBooleanArg(args: Record, key: string): boolean | undefined { + const v = args[key]; + if (v === true || v === "true") return true; + if (v === false || v === "false") return false; + return undefined; +} From 90f1f0be1ae1cf3433246da2101b1853c3250c6b Mon Sep 17 00:00:00 2001 From: Igor Galeta Date: Thu, 9 Jul 2026 20:11:00 +0300 Subject: [PATCH 2/6] docs: add enabled flag, clarify provider scope --- index.ts | 2 +- openclaw.plugin.json | 2 +- skills/serpapi/SKILL.md | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/index.ts b/index.ts index 5d03ad1..d9713fd 100644 --- a/index.ts +++ b/index.ts @@ -40,7 +40,7 @@ const plugin: OpenClawPluginDefinition = definePluginEntry({ id: "serpapi", name: "SerpApi Search", description: - "Web search provider plus specialized SerpApi tools for news, flights, hotels, maps, shopping, YouTube, scholar, finance, events, and 100+ search engines.", + "SerpApi-powered web search provider (Google Light) plus specialized tools for news, flights, hotels, maps, shopping, YouTube, scholar, finance, events, and more.", register(api) { api.registerWebSearchProvider(createSerpApiWebSearchProvider()); api.registerTool((ctx) => createSerpApiAmazonTool(api, ctx), { name: "serpapi_amazon" }); diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 380a0d9..52329e7 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -1,7 +1,7 @@ { "id": "serpapi", "name": "SerpApi Search", - "description": "Web search provider plus specialized SerpApi tools for news, flights, hotels, maps, shopping, YouTube, scholar, finance, events, and 100+ search engines.", + "description": "SerpApi-powered web search provider (Google Light) plus specialized tools for news, flights, hotels, maps, shopping, YouTube, scholar, finance, events, and more.", "activation": { "onStartup": false }, diff --git a/skills/serpapi/SKILL.md b/skills/serpapi/SKILL.md index b5591cd..955a92c 100644 --- a/skills/serpapi/SKILL.md +++ b/skills/serpapi/SKILL.md @@ -9,13 +9,15 @@ metadata: ## Configuration -Set your API key and optional default language under `config.webSearch`: +Set your API key and optional default language under `config.webSearch`. The +plugin must also be enabled via `plugins.entries.serpapi.enabled`: ```json { "plugins": { "entries": { "serpapi": { + "enabled": true, "config": { "webSearch": { "apiKey": "your-serpapi-key", From 6718f3ac86ec135030753373cbac209689644de9 Mon Sep 17 00:00:00 2001 From: Igor Galeta Date: Thu, 9 Jul 2026 21:57:02 +0300 Subject: [PATCH 3/6] fix: hotels date validation --- src/tools/hotels.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/hotels.ts b/src/tools/hotels.ts index de795b8..2235918 100644 --- a/src/tools/hotels.ts +++ b/src/tools/hotels.ts @@ -120,7 +120,7 @@ export function createSerpApiHotelsTool(api: OpenClawPluginApi, ctx?: SerpApiToo const checkOut = rawCheckOut ? parseIsoDate(rawCheckOut, "check_out_date") : isoDateOffsetFrom(checkIn, 2); - if (rawCheckIn && rawCheckOut && checkOut <= checkIn) { + if (checkOut <= checkIn) { throw new Error("serpapi_hotels: check_out_date must be after check_in_date"); } const raw = await callSerpApi({ From db704eebe87182f446be420a43b0dfe55d64ed0b Mon Sep 17 00:00:00 2001 From: Igor Galeta Date: Fri, 10 Jul 2026 12:54:46 +0300 Subject: [PATCH 4/6] fix: switch cat to number/integer --- src/tools/flights.ts | 16 ++++++++++++---- src/tools/hotels.ts | 2 +- src/tools/trends.ts | 4 ++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/tools/flights.ts b/src/tools/flights.ts index cf0d352..0c93bd9 100644 --- a/src/tools/flights.ts +++ b/src/tools/flights.ts @@ -63,6 +63,14 @@ export function createSerpApiFlightsTool(api: OpenClawPluginApi, ctx?: SerpApiTo }, execute: async (_toolCallId: string, args: Record, signal?: AbortSignal) => { const cfg = resolveToolConfig(api, ctx); + const type = readStringParam(args, "type") ?? "1"; + const returnDate = readStringParam(args, "return_date") ?? undefined; + if (type === "1" && !returnDate) { + throw new Error( + "serpapi_flights: return_date is required for round-trip searches (type=1). " + + "Provide return_date or set type=2 for a one-way search.", + ); + } const raw = await callSerpApi({ cfg, engine: "google_flights", @@ -71,10 +79,10 @@ export function createSerpApiFlightsTool(api: OpenClawPluginApi, ctx?: SerpApiTo departure_id: readStringParam(args, "departure_id", { required: true }), arrival_id: readStringParam(args, "arrival_id", { required: true }), outbound_date: readStringParam(args, "outbound_date", { required: true }), - return_date: readStringParam(args, "return_date") ?? undefined, - type: readStringParam(args, "type") ?? undefined, - adults: readNumberParam(args, "adults", { integer: true }) ?? undefined, - currency: readStringParam(args, "currency") ?? undefined, + return_date: returnDate, + type, + adults: readNumberParam(args, "adults", { integer: true }) ?? 1, + currency: readStringParam(args, "currency") ?? "USD", gl: readStringParam(args, "gl") ?? undefined, }, signal, diff --git a/src/tools/hotels.ts b/src/tools/hotels.ts index 2235918..c6aba14 100644 --- a/src/tools/hotels.ts +++ b/src/tools/hotels.ts @@ -131,7 +131,7 @@ export function createSerpApiHotelsTool(api: OpenClawPluginApi, ctx?: SerpApiToo q: readStringParam(args, "query", { required: true }), check_in_date: checkIn, check_out_date: checkOut, - adults: readNumberParam(args, "adults", { integer: true }) ?? undefined, + adults: readNumberParam(args, "adults", { integer: true }) ?? 1, currency: readStringParam(args, "currency") ?? undefined, gl: readStringParam(args, "gl") ?? undefined, sort_by: readNumberParam(args, "sort_by", { integer: true }) ?? undefined, diff --git a/src/tools/trends.ts b/src/tools/trends.ts index cbcf38c..3c3ab3b 100644 --- a/src/tools/trends.ts +++ b/src/tools/trends.ts @@ -63,7 +63,7 @@ export function createSerpApiTrendsTool(api: OpenClawPluginApi, ctx?: SerpApiToo description: "Region breakdown level for GEO_MAP/GEO_MAP_0 data types.", }, cat: { - type: "string", + type: "number", description: "Search category ID (default: 0 = all categories).", }, gprop: { @@ -93,7 +93,7 @@ export function createSerpApiTrendsTool(api: OpenClawPluginApi, ctx?: SerpApiToo date: readStringParam(args, "date") ?? "today 12-m", geo: readStringParam(args, "geo") ?? undefined, region: readStringParam(args, "region") ?? undefined, - cat: readStringParam(args, "cat") ?? undefined, + cat: readNumberParam(args, "cat", { integer: true }) ?? undefined, gprop: readStringParam(args, "gprop") ?? undefined, tz: readNumberParam(args, "tz", { integer: true }) ?? undefined, }, From d3d947219525e0db40a3e1378b578483b5c9c9ee Mon Sep 17 00:00:00 2001 From: Igor Galeta Date: Fri, 10 Jul 2026 17:09:25 +0300 Subject: [PATCH 5/6] docs: align tool defaults and schemas with SKILL.md documentation --- skills/serpapi/SKILL.md | 9 +++++++-- src/tools/amazon-product.ts | 2 +- src/tools/amazon.ts | 3 ++- src/tools/bing.ts | 2 +- src/tools/duckduckgo.ts | 3 ++- src/tools/ebay.ts | 4 ++-- src/tools/maps-reviews.ts | 6 ++++-- src/tools/trends.ts | 4 ++-- src/tools/tripadvisor.ts | 2 +- src/tools/walmart.ts | 2 +- src/tools/yahoo.ts | 2 +- src/tools/youtube-transcript.ts | 2 +- 12 files changed, 25 insertions(+), 16 deletions(-) diff --git a/skills/serpapi/SKILL.md b/skills/serpapi/SKILL.md index 955a92c..33e18c5 100644 --- a/skills/serpapi/SKILL.md +++ b/skills/serpapi/SKILL.md @@ -115,6 +115,11 @@ Search Google Scholar for academic papers. Results include `inline_links.cited_by.total` (citation count) and `result_id` for follow-up `cites`/`cluster` calls. +### Tips + +- At least one of `query`, `cites`, or `cluster` is required. +- `cluster` cannot be combined with `query` or `cites`. + ## serpapi_maps Find local businesses and places via Google Maps. @@ -145,7 +150,7 @@ Search Amazon for products across any marketplace. | `query` | Product search query | | `amazon_domain` | Marketplace domain (e.g. `amazon.com`, `amazon.de`, `amazon.co.uk`, `amazon.co.jp`) | | `language` | Locale code (e.g. `en_US`, `de_DE`, `ja_JP`) | -| `s` | Sort: `price-asc-rank`, `price-desc-rank`, `review-rank`, `date-desc-rank`, `exact-aware-popularity-rank` | +| `s` | Sort: `relevanceblender`=Featured (default), `price-asc-rank`, `price-desc-rank`, `review-rank`, `date-desc-rank`, `exact-aware-popularity-rank` | | `node` | Category node ID (from Amazon URL or `filters[].node` in a previous response) | | `rh` | Attribute filter string from `filters[].rh` in a previous response | | `page` | Page number for pagination (default: 1) | @@ -282,7 +287,7 @@ Look up stock prices, cryptocurrency, FX rates, and market data via Google Finan | Parameter | Description | | --------- | ----------------------------------------------------------------------- | -| `query` | Ticker or pair (e.g. `AAPL`, `BTC-USD`, `EUR-USD`, `NASDAQ:GOOGL`) | +| `query` | Ticker or pair (e.g. `AAPL`, `BTC-USD`, `USDEUR=X`, `NASDAQ:GOOGL`) | | `window` | Time window: `1D` (default), `5D`, `1M`, `6M`, `YTD`, `1Y`, `5Y`, `MAX` | Response includes `summary` (price, movement, exchange), `markets` (US/Europe/Asia indices, currencies, crypto), `graph` (price history points), `knowledge_graph` (key stats), `financials` (income statement), `news_results`, and `discover_more`. diff --git a/src/tools/amazon-product.ts b/src/tools/amazon-product.ts index 3f77990..5e0f173 100644 --- a/src/tools/amazon-product.ts +++ b/src/tools/amazon-product.ts @@ -84,7 +84,7 @@ export function createSerpApiAmazonProductTool(api: OpenClawPluginApi, ctx?: Ser allowedParams: ALLOWED_PARAMS, params: { asin: readStringParam(args, "asin", { required: true }), - amazon_domain: readStringParam(args, "amazon_domain") ?? undefined, + amazon_domain: readStringParam(args, "amazon_domain") ?? "amazon.com", language: readStringParam(args, "language") ?? undefined, delivery_zip: readStringParam(args, "delivery_zip") ?? undefined, shipping_location: readStringParam(args, "shipping_location") ?? undefined, diff --git a/src/tools/amazon.ts b/src/tools/amazon.ts index 044e2a4..0c64b80 100644 --- a/src/tools/amazon.ts +++ b/src/tools/amazon.ts @@ -51,6 +51,7 @@ export function createSerpApiAmazonTool(api: OpenClawPluginApi, ctx?: SerpApiToo s: { type: "string", enum: [ + "relevanceblender", "relevanceblender", "price-asc-rank", "price-desc-rank", @@ -91,7 +92,7 @@ export function createSerpApiAmazonTool(api: OpenClawPluginApi, ctx?: SerpApiToo s: readStringParam(args, "s") ?? undefined, node: readStringParam(args, "node") ?? undefined, rh: readStringParam(args, "rh") ?? undefined, - page: readNumberParam(args, "page", { integer: true }) ?? undefined, + page: readNumberParam(args, "page", { integer: true }) ?? 1, }, signal, }); diff --git a/src/tools/bing.ts b/src/tools/bing.ts index f503fe9..c716aa8 100644 --- a/src/tools/bing.ts +++ b/src/tools/bing.ts @@ -98,7 +98,7 @@ export function createSerpApiBingTool(api: OpenClawPluginApi, ctx?: SerpApiToolC cc: cc ?? undefined, location: readStringParam(args, "location") ?? undefined, safeSearch: readStringParam(args, "safeSearch") ?? undefined, - first: readNumberParam(args, "first", { integer: true }) ?? undefined, + first: readNumberParam(args, "first", { integer: true }) ?? 1, }, signal, }); diff --git a/src/tools/duckduckgo.ts b/src/tools/duckduckgo.ts index 09d4e0a..20da528 100644 --- a/src/tools/duckduckgo.ts +++ b/src/tools/duckduckgo.ts @@ -109,6 +109,7 @@ export function createSerpApiDuckDuckGoTool(api: OpenClawPluginApi, ctx?: SerpAp if (searchAssist === true && m != null) { throw new Error("serpapi_duckduckgo: search_assist and m are mutually exclusive"); } + const resolvedM = searchAssist === true ? undefined : (m ?? 50); const raw = await callSerpApi({ cfg, engine: "duckduckgo", @@ -118,7 +119,7 @@ export function createSerpApiDuckDuckGoTool(api: OpenClawPluginApi, ctx?: SerpAp kl: readStringParam(args, "kl") ?? undefined, safe: readNumberParam(args, "safe", { integer: true }) ?? undefined, df: readStringParam(args, "df") ?? undefined, - m: m ?? undefined, + m: resolvedM, start: readNumberParam(args, "start", { integer: true }) ?? undefined, search_assist: searchAssistParam, }, diff --git a/src/tools/ebay.ts b/src/tools/ebay.ts index c03fa87..d963514 100644 --- a/src/tools/ebay.ts +++ b/src/tools/ebay.ts @@ -130,8 +130,8 @@ export function createSerpApiEbayTool(api: OpenClawPluginApi, ctx?: SerpApiToolC category_id: categoryId ?? undefined, LH_ItemCondition: readStringParam(args, "condition") ?? undefined, _stpos: readStringParam(args, "zip") ?? undefined, - _pgn: readNumberParam(args, "page", { integer: true }) ?? undefined, - _ipg: readNumberParam(args, "per_page", { integer: true }) ?? undefined, + _pgn: readNumberParam(args, "page", { integer: true }) ?? 1, + _ipg: readNumberParam(args, "per_page", { integer: true }) ?? 50, }, signal, }); diff --git a/src/tools/maps-reviews.ts b/src/tools/maps-reviews.ts index 45b6ed6..7ef9fbe 100644 --- a/src/tools/maps-reviews.ts +++ b/src/tools/maps-reviews.ts @@ -116,11 +116,13 @@ export function createSerpApiMapsReviewsTool(api: OpenClawPluginApi, ctx?: SerpA } const nextPageToken = readStringParam(args, "next_page_token"); const num = readNumberParam(args, "num", { integer: true }) ?? undefined; - if (num !== undefined && !nextPageToken && !topicId && !query) { + const numAllowed = Boolean(nextPageToken || topicId || query); + if (num !== undefined && !numAllowed) { throw new Error( "serpapi_maps_reviews: num cannot be used on the initial page without next_page_token, topic_id, or query", ); } + const resolvedNum = numAllowed ? (num ?? 10) : undefined; const raw = await callSerpApi({ cfg, engine: "google_maps_reviews", @@ -132,7 +134,7 @@ export function createSerpApiMapsReviewsTool(api: OpenClawPluginApi, ctx?: SerpA sort_by: readStringParam(args, "sort_by") ?? undefined, topic_id: topicId ?? undefined, query: query ?? undefined, - num, + num: resolvedNum, next_page_token: nextPageToken ?? undefined, }, signal, diff --git a/src/tools/trends.ts b/src/tools/trends.ts index 3c3ab3b..bdd0b6f 100644 --- a/src/tools/trends.ts +++ b/src/tools/trends.ts @@ -93,9 +93,9 @@ export function createSerpApiTrendsTool(api: OpenClawPluginApi, ctx?: SerpApiToo date: readStringParam(args, "date") ?? "today 12-m", geo: readStringParam(args, "geo") ?? undefined, region: readStringParam(args, "region") ?? undefined, - cat: readNumberParam(args, "cat", { integer: true }) ?? undefined, + cat: readNumberParam(args, "cat", { integer: true }) ?? 0, gprop: readStringParam(args, "gprop") ?? undefined, - tz: readNumberParam(args, "tz", { integer: true }) ?? undefined, + tz: readNumberParam(args, "tz", { integer: true }) ?? 420, }, signal, }); diff --git a/src/tools/tripadvisor.ts b/src/tools/tripadvisor.ts index 9e38f3f..f712877 100644 --- a/src/tools/tripadvisor.ts +++ b/src/tools/tripadvisor.ts @@ -91,7 +91,7 @@ export function createSerpApiTripadvisorTool(api: OpenClawPluginApi, ctx?: SerpA tripadvisor_domain: readStringParam(args, "tripadvisor_domain") ?? undefined, lat: readNumberParam(args, "lat") ?? undefined, lon: readNumberParam(args, "lon") ?? undefined, - limit: readNumberParam(args, "limit", { integer: true }) ?? undefined, + limit: readNumberParam(args, "limit", { integer: true }) ?? 30, offset: readNumberParam(args, "offset", { integer: true }) ?? undefined, }, signal, diff --git a/src/tools/walmart.ts b/src/tools/walmart.ts index 2071f90..997216d 100644 --- a/src/tools/walmart.ts +++ b/src/tools/walmart.ts @@ -108,7 +108,7 @@ export function createSerpApiWalmartTool(api: OpenClawPluginApi, ctx?: SerpApiTo store_id: readStringParam(args, "store_id") ?? undefined, min_price: readNumberParam(args, "min_price") ?? undefined, max_price: readNumberParam(args, "max_price") ?? undefined, - page: readNumberParam(args, "page", { integer: true }) ?? undefined, + page: readNumberParam(args, "page", { integer: true }) ?? 1, }, signal, }); diff --git a/src/tools/yahoo.ts b/src/tools/yahoo.ts index e8afc35..f430744 100644 --- a/src/tools/yahoo.ts +++ b/src/tools/yahoo.ts @@ -105,7 +105,7 @@ export function createSerpApiYahooTool(api: OpenClawPluginApi, ctx?: SerpApiTool vm: readStringParam(args, "vm") ?? undefined, vs: readStringParam(args, "vs") ?? undefined, vf: readStringParam(args, "vf") ?? undefined, - b: readNumberParam(args, "b", { integer: true }) ?? undefined, + b: readNumberParam(args, "b", { integer: true }) ?? 1, }, signal, }); diff --git a/src/tools/youtube-transcript.ts b/src/tools/youtube-transcript.ts index a3e4b92..bc24171 100644 --- a/src/tools/youtube-transcript.ts +++ b/src/tools/youtube-transcript.ts @@ -58,7 +58,7 @@ export function createSerpApiYouTubeTranscriptTool(api: OpenClawPluginApi, ctx?: allowedParams: ALLOWED_PARAMS, params: { v: readStringParam(args, "v", { required: true }), - language_code: readStringParam(args, "language_code") ?? undefined, + language_code: readStringParam(args, "language_code") ?? "en", title: readStringParam(args, "title") ?? undefined, type: readStringParam(args, "type") ?? undefined, }, From cb6aa2063aab3c61a6bf6544fad6ab6802ec98e2 Mon Sep 17 00:00:00 2001 From: Igor Galeta Date: Fri, 10 Jul 2026 18:43:30 +0300 Subject: [PATCH 6/6] fix: remove duplicated relevanceblender value for s param --- src/tools/amazon.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tools/amazon.ts b/src/tools/amazon.ts index 0c64b80..ca6cd5e 100644 --- a/src/tools/amazon.ts +++ b/src/tools/amazon.ts @@ -51,7 +51,6 @@ export function createSerpApiAmazonTool(api: OpenClawPluginApi, ctx?: SerpApiToo s: { type: "string", enum: [ - "relevanceblender", "relevanceblender", "price-asc-rank", "price-desc-rank",