From 844edc2bc626edf03c49135ea5789799fd38d88f Mon Sep 17 00:00:00 2001 From: Igor Galeta Date: Thu, 20 Aug 2026 10:58:01 +0300 Subject: [PATCH] feat: default serpapi tools to markdown output, add md/json format setting --- README.md | 22 +++++++- openclaw.plugin.json | 10 ++++ package.json | 2 +- skills/serpapi/SKILL.md | 19 ++++++- src/config.ts | 16 ++++++ src/markdown.ts | 99 +++++++++++++++++++++++++++++++++ src/serpapi-client.ts | 50 +++++++++++++++-- src/serpapi-search-provider.ts | 4 +- src/tools/amazon-product.ts | 12 +++- src/tools/amazon.ts | 12 +++- src/tools/autocomplete.ts | 12 +++- src/tools/bing.ts | 12 +++- src/tools/duckduckgo.ts | 12 +++- src/tools/ebay-product.ts | 12 +++- src/tools/ebay.ts | 12 +++- src/tools/events.ts | 12 +++- src/tools/facebook-profile.ts | 12 +++- src/tools/finance.ts | 12 +++- src/tools/flights.ts | 12 +++- src/tools/hotels.ts | 12 +++- src/tools/immersive-product.ts | 12 +++- src/tools/instagram-profile.ts | 12 +++- src/tools/jobs.ts | 15 ++++- src/tools/lens.ts | 12 +++- src/tools/maps-reviews.ts | 12 +++- src/tools/maps.ts | 15 ++++- src/tools/news.ts | 15 ++++- src/tools/scholar.ts | 15 ++++- src/tools/shopping.ts | 15 ++++- src/tools/trends.ts | 12 +++- src/tools/tripadvisor.ts | 12 +++- src/tools/walmart-product.ts | 12 +++- src/tools/walmart.ts | 12 +++- src/tools/weather.ts | 12 +++- src/tools/yahoo.ts | 12 +++- src/tools/youtube-transcript.ts | 12 +++- src/tools/youtube-video.ts | 12 +++- src/tools/youtube.ts | 12 +++- src/utils.ts | 31 +++++++++++ 39 files changed, 528 insertions(+), 100 deletions(-) create mode 100644 src/markdown.ts diff --git a/README.md b/README.md index ee350e1..15ee841 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Set the API key via config or the `SERPAPI_API_KEY` environment variable: entries: { serpapi: { enabled: true, - config: { webSearch: { apiKey: "YOUR_SERPAPI_KEY", hl: "en" } }, + config: { webSearch: { apiKey: "YOUR_SERPAPI_KEY", hl: "en", format: "md" } }, }, }, }, @@ -58,6 +58,26 @@ export SERPAPI_API_KEY="YOUR_SERPAPI_KEY" Get a key at [serpapi.com/users/sign_up](https://serpapi.com/users/sign_up). +### Response format + +> **Breaking change in 0.2.0** — the specialized `serpapi_*` tools now return +> markdown by default. + +`format` controls how the specialized `serpapi_*` tools return results: + +- **`"md"` (default)** — SerpApi's markdown rendering, optimized for LLMs. + Results are returned under a single `markdown` field wrapped in + untrusted-content security markers, with the results table truncated to the + requested `count`. +- **`"json"`** — the previous structured JSON responses, useful when you need + to process specific fields programmatically. + +Every specialized tool also accepts a per-call `output` argument (`"md"` or +`"json"`) that overrides the configured default. + +The `web_search` provider always uses JSON: its contract (`results[]`, +`count`/`maxResults`) is structured, so markdown does not apply to it. + ## Develop Requires Node 22.19+ and [pnpm](https://pnpm.io). diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 52329e7..3a7d72c 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -25,6 +25,10 @@ "webSearch.hl": { "label": "Default Language", "help": "Default language code for all searches (e.g. en, de, uk). Defaults to en." + }, + "webSearch.format": { + "label": "Response Format", + "help": "Response format for all SerpApi requests: 'md' (markdown, default, fewer LLM tokens) or 'json' (structured)." } }, "contracts": { @@ -76,6 +80,12 @@ "hl": { "type": "string", "description": "Default language code for all searches (e.g. en, de, ja). Defaults to en." + }, + "format": { + "type": "string", + "enum": ["md", "json"], + "default": "md", + "description": "Response format: 'md' returns SerpApi's LLM-optimized markdown (default, fewer tokens); 'json' returns structured JSON." } } } diff --git a/package.json b/package.json index c72c658..ec51f86 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@serpapi/openclaw-plugin", - "version": "0.1.0", + "version": "0.2.0", "description": "SerpApi web search provider for OpenClaw (Google Light, 100+ engines). Specialized SerpApi tools follow in subsequent releases.", "keywords": [ "openclaw", diff --git a/skills/serpapi/SKILL.md b/skills/serpapi/SKILL.md index 33e18c5..4687f96 100644 --- a/skills/serpapi/SKILL.md +++ b/skills/serpapi/SKILL.md @@ -21,7 +21,8 @@ plugin must also be enabled via `plugins.entries.serpapi.enabled`: "config": { "webSearch": { "apiKey": "your-serpapi-key", - "hl": "en" + "hl": "en", + "format": "md" } } } @@ -32,6 +33,22 @@ plugin must also be enabled via `plugins.entries.serpapi.enabled`: `apiKey` can also be provided via the `SERPAPI_API_KEY` environment variable. `hl` defaults to `en`. +## Response format (breaking change in 0.2.0) + +`format` selects the default response format for all `serpapi_*` tools: + +- `"md"` (default) — SerpApi's LLM-optimized markdown. Tool results are + returned under a single `markdown` field wrapped in untrusted-content + security markers, with the results table truncated to `count` where the + tool supports it. +- `"json"` — the structured JSON responses documented per tool below. + +Each tool also accepts a per-call `output` argument (`"md"` or `"json"`) to +override the configured default. + +The `web_search` provider always uses JSON; the markdown format applies only +to the specialized tools. + ## When to use which tool | Need | Tool | diff --git a/src/config.ts b/src/config.ts index 98829df..e52415e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,9 +12,12 @@ export const SERPAPI_DEFAULT_TIMEOUT_SECONDS = 30; // Slightly under SerpApi's 1-hour server-side cache window so we refresh before it expires. export const SERPAPI_CACHE_TTL_MS = 55 * 60_000; +export type SerpApiOutputFormat = "md" | "json"; + type SerpApiConfig = { apiKey?: unknown; hl?: unknown; + format?: unknown; }; export function resolveSerpApiPluginConfig(cfg?: OpenClawConfig): SerpApiConfig | undefined { @@ -34,3 +37,16 @@ export function resolveSerpApiLanguage(cfg?: OpenClawConfig): string { const pluginConfig = resolveSerpApiPluginConfig(cfg); return normalizeOptionalString(pluginConfig?.hl)?.trim() || "en"; } + +/** Invalid values throw instead of silently falling back. */ +export function resolveSerpApiFormat(cfg?: OpenClawConfig): SerpApiOutputFormat { + const pluginConfig = resolveSerpApiPluginConfig(cfg); + const format = normalizeOptionalString(pluginConfig?.format)?.trim().toLowerCase(); + if (format === undefined) { + return "md"; + } + if (format === "md" || format === "json") { + return format; + } + throw new Error(`serpapi: invalid webSearch.format "${format}". Expected "md" or "json".`); +} diff --git a/src/markdown.ts b/src/markdown.ts new file mode 100644 index 0000000..b989548 --- /dev/null +++ b/src/markdown.ts @@ -0,0 +1,99 @@ +/** Limits result-table rows under a named heading; other sections stay unchanged. */ + +function normalizeHeading(value: string): string { + return value + .toLowerCase() + .replace(/[^a-z0-9]+/g, " ") + .trim(); +} + +function matchesHeading(value: string, expected: string): boolean { + const normalized = normalizeHeading(value); + const want = normalizeHeading(expected); + if (normalized === want || normalized.startsWith(`${want} `)) { + return true; + } + // "News Results (5)" normalizes to "news results 5". + const rest = normalized.startsWith(want) ? normalized.slice(want.length) : undefined; + return rest !== undefined && /^\s*\d/.test(rest); +} + +function atxHeadingText(line: string): { level: number; text: string } | undefined { + const m = /^(#{1,6})\s+(.*)$/.exec(line); + if (!m || m[1] === undefined || m[2] === undefined) return undefined; + return { level: m[1].length, text: m[2].replace(/#+\s*$/, "").trim() }; +} + +function isTableRow(line: string): boolean { + const trimmed = line.trim(); + return trimmed.startsWith("|") && trimmed.endsWith("|"); +} + +function isTableSeparator(line: string): boolean { + return /^\|?[\s:|-]+\|[\s:|-]*$/.test(line.trim()) && line.includes("-"); +} + +export function limitResultTable(markdown: string, opts: { heading: string; limit: number }): string { + const { heading, limit } = opts; + const lines = markdown.split("\n"); + + let sectionLevel: number | undefined; + let sectionStart = -1; + let sectionEnd = lines.length; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line === undefined) continue; + const h = atxHeadingText(line); + if (!h) continue; + if (sectionLevel === undefined) { + if (matchesHeading(h.text, heading)) { + sectionLevel = h.level; + sectionStart = i; + } + continue; + } + if (h.level <= sectionLevel) { + sectionEnd = i; + break; + } + } + + if (sectionLevel === undefined || sectionStart < 0) { + return markdown; + } + + let headerSeen = false; + let separatorSeen = false; + let rowCount = 0; + const removed = new Set(); + + for (let i = sectionStart + 1; i < sectionEnd; i++) { + const line = lines[i]; + if (line === undefined) continue; + if (!isTableRow(line)) { + if (separatorSeen && line.trim() === "") break; + continue; + } + if (!headerSeen) { + headerSeen = true; + continue; + } + if (!separatorSeen) { + if (isTableSeparator(line)) { + separatorSeen = true; + } + continue; + } + rowCount++; + if (rowCount > limit) { + removed.add(i); + } + } + + if (removed.size === 0) { + return markdown; + } + + return lines.filter((_, i) => !removed.has(i)).join("\n"); +} diff --git a/src/serpapi-client.ts b/src/serpapi-client.ts index b9822e1..9f5c290 100644 --- a/src/serpapi-client.ts +++ b/src/serpapi-client.ts @@ -6,11 +6,13 @@ import { writeCachedSearchPayload, } from "openclaw/plugin-sdk/provider-web-search"; import { + resolveSerpApiFormat, resolveSerpApiKey, resolveSerpApiLanguage, SERPAPI_BASE_URL, SERPAPI_CACHE_TTL_MS, SERPAPI_DEFAULT_TIMEOUT_SECONDS, + type SerpApiOutputFormat, } from "./config.js"; // In-process result cache — aligns with SerpApi's 1-hour server-side cache window. @@ -27,11 +29,40 @@ export type SerpApiCallParams = { */ allowedParams: readonly string[]; params: Record; + output?: SerpApiOutputFormat; timeoutSeconds?: number; signal?: AbortSignal; }; -export async function callSerpApi(opts: SerpApiCallParams): Promise> { +export type SerpApiResponse = Record | string; + +/** The shared search cache stores records, so markdown payloads are wrapped. */ +const MARKDOWN_CACHE_KEY = "__serpapi_markdown"; + +function wrapForCache(response: SerpApiResponse): Record { + return typeof response === "string" ? { [MARKDOWN_CACHE_KEY]: response } : response; +} + +function unwrapFromCache(cached: Record): SerpApiResponse { + const markdown = cached[MARKDOWN_CACHE_KEY]; + return typeof markdown === "string" ? markdown : cached; +} + +function redactApiKey(text: string, apiKey: string): string { + return text.split(apiKey).join("[redacted]"); +} + +function resolveOutputFormat(cfg: OpenClawConfig | undefined, override?: SerpApiOutputFormat): SerpApiOutputFormat { + if (override !== undefined) { + if (override !== "md" && override !== "json") { + throw new Error(`serpapi: output must be "md" or "json", got "${override}"`); + } + return override; + } + return resolveSerpApiFormat(cfg); +} + +export async function callSerpApi(opts: SerpApiCallParams): Promise { const apiKey = resolveSerpApiKey(opts.cfg); if (!apiKey) { throw new Error( @@ -41,6 +72,7 @@ export async function callSerpApi(opts: SerpApiCallParams): Promise = {}; @@ -60,6 +92,10 @@ export async function callSerpApi(opts: SerpApiCallParams): Promise k === "engine" || k === "hl" || allowed.has(k)), ); + if (format === "md") { + filtered.output = "md"; + } + const isZeroTrace = filtered.zero_trace === "true"; const cacheKey = buildSearchCacheKey([ @@ -73,7 +109,7 @@ export async function callSerpApi(opts: SerpApiCallParams): Promise= 500) throw new Error(`SerpApi: upstream error (${response.status}). Try again shortly.`); - throw new Error(`SerpApi (${opts.engine}) error (${response.status}): ${text}`); + throw new Error(`SerpApi (${opts.engine}) error (${response.status}): ${redactApiKey(text, apiKey)}`); + } + const text = redactApiKey(await response.text(), apiKey); + if (format === "md") { + return text; } - const text = await response.text(); try { return JSON.parse(text) as Record; } catch { @@ -110,7 +148,7 @@ export async function callSerpApi(opts: SerpApiCallParams): Promise, count); }, }), }; diff --git a/src/tools/amazon-product.ts b/src/tools/amazon-product.ts index 456f72b..75bf3a1 100644 --- a/src/tools/amazon-product.ts +++ b/src/tools/amazon-product.ts @@ -1,8 +1,8 @@ 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 { readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = [ "asin", @@ -65,6 +65,11 @@ export function createSerpApiAmazonProductTool(api: OpenClawPluginApi, ctx?: Ser type: "string", description: "Country to filter shipping products by.", }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["asin"], additionalProperties: false, @@ -82,9 +87,10 @@ export function createSerpApiAmazonProductTool(api: OpenClawPluginApi, ctx?: Ser delivery_zip: readStringParam(args, "delivery_zip") ?? undefined, shipping_location: readStringParam(args, "shipping_location") ?? undefined, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/amazon.ts b/src/tools/amazon.ts index c5edcf8..3006d43 100644 --- a/src/tools/amazon.ts +++ b/src/tools/amazon.ts @@ -1,8 +1,8 @@ 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 { readNumberParam, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["k", "amazon_domain", "language", "s", "node", "rh", "page", "zero_trace"] as const; @@ -60,6 +60,11 @@ export function createSerpApiAmazonTool(api: OpenClawPluginApi, ctx?: SerpApiToo description: "Page number for pagination (default: 1).", minimum: 1, }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["query"], additionalProperties: false, @@ -79,9 +84,10 @@ export function createSerpApiAmazonTool(api: OpenClawPluginApi, ctx?: SerpApiToo rh: readStringParam(args, "rh") ?? undefined, page: readNumberParam(args, "page", { integer: true }) ?? 1, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/autocomplete.ts b/src/tools/autocomplete.ts index d6345a4..312d349 100644 --- a/src/tools/autocomplete.ts +++ b/src/tools/autocomplete.ts @@ -1,8 +1,8 @@ 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 { readNumberParam, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["q", "gl", "hl", "cp", "client", "zero_trace"] as const; @@ -46,6 +46,11 @@ export function createSerpApiAutocompleteTool(api: OpenClawPluginApi, ctx?: Serp type: "string", description: "Autocomplete client (e.g. chrome, safari, firefox-b-d, youtube). Affects suggestion style.", }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["query"], additionalProperties: false, @@ -63,9 +68,10 @@ export function createSerpApiAutocompleteTool(api: OpenClawPluginApi, ctx?: Serp cp: readNumberParam(args, "cp", { integer: true }) ?? undefined, client: readStringParam(args, "client") ?? undefined, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/bing.ts b/src/tools/bing.ts index b86640e..0d982ee 100644 --- a/src/tools/bing.ts +++ b/src/tools/bing.ts @@ -1,8 +1,8 @@ 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 { readNumberParam, readStringParam, wrapWebContent } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["q", "cc", "mkt", "location", "safeSearch", "filters", "first", "zero_trace"] as const; @@ -57,6 +57,11 @@ export function createSerpApiBingTool(api: OpenClawPluginApi, ctx?: SerpApiToolC description: "Result offset for pagination (default: 1; use 11 for page 2, 21 for page 3, ...).", minimum: 1, }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["query"], additionalProperties: false, @@ -80,9 +85,10 @@ export function createSerpApiBingTool(api: OpenClawPluginApi, ctx?: SerpApiToolC safeSearch: readStringParam(args, "safeSearch") ?? "Moderate", first: readNumberParam(args, "first", { integer: true }) ?? 1, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/duckduckgo.ts b/src/tools/duckduckgo.ts index f3d2eee..71378de 100644 --- a/src/tools/duckduckgo.ts +++ b/src/tools/duckduckgo.ts @@ -1,8 +1,8 @@ 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 { readNumberParam, readStringParam, wrapWebContent } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { readBooleanArg, resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readBooleanArg, readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["q", "kl", "safe", "df", "m", "start", "search_assist", "zero_trace"] as const; @@ -75,6 +75,11 @@ export function createSerpApiDuckDuckGoTool(api: OpenClawPluginApi, ctx?: SerpAp type: "boolean", description: "Include DuckDuckGo AI Search Assist answer in the response. Cannot be used with m.", }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["query"], additionalProperties: false, @@ -101,9 +106,10 @@ export function createSerpApiDuckDuckGoTool(api: OpenClawPluginApi, ctx?: SerpAp start: readNumberParam(args, "start", { integer: true }) ?? undefined, search_assist: searchAssistParam, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/ebay-product.ts b/src/tools/ebay-product.ts index 79ed609..bf07694 100644 --- a/src/tools/ebay-product.ts +++ b/src/tools/ebay-product.ts @@ -1,8 +1,8 @@ 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 { readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["product_id", "ebay_domain", "locale", "lang", "shipping_country", "zero_trace"] as const; @@ -46,6 +46,11 @@ export function createSerpApiEbayProductTool(api: OpenClawPluginApi, ctx?: SerpA type: "string", description: "Country code for shipping cost calculation (e.g. US, GB, DE).", }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["product_id"], additionalProperties: false, @@ -63,9 +68,10 @@ export function createSerpApiEbayProductTool(api: OpenClawPluginApi, ctx?: SerpA lang: readStringParam(args, "lang") ?? undefined, shipping_country: readStringParam(args, "shipping_country") ?? undefined, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/ebay.ts b/src/tools/ebay.ts index 5efb687..14de4c8 100644 --- a/src/tools/ebay.ts +++ b/src/tools/ebay.ts @@ -1,8 +1,8 @@ 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 { readNumberParam, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = [ "_nkw", @@ -97,6 +97,11 @@ export function createSerpApiEbayTool(api: OpenClawPluginApi, ctx?: SerpApiToolC enum: [25, 50, 100, 200], description: "Results per page: 25, 50 (default), 100, or 200.", }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: [], additionalProperties: false, @@ -126,9 +131,10 @@ export function createSerpApiEbayTool(api: OpenClawPluginApi, ctx?: SerpApiToolC _pgn: readNumberParam(args, "page", { integer: true }) ?? 1, _ipg: readNumberParam(args, "per_page", { integer: true }) ?? 50, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/events.ts b/src/tools/events.ts index 994c924..3cbcd7c 100644 --- a/src/tools/events.ts +++ b/src/tools/events.ts @@ -1,8 +1,8 @@ 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 { readNumberParam, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["q", "gl", "hl", "location", "htichips", "start", "zero_trace"] as const; @@ -30,6 +30,11 @@ export function createSerpApiEventsTool(api: OpenClawPluginApi, ctx?: SerpApiToo 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...)." }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["query"], additionalProperties: false, @@ -48,9 +53,10 @@ export function createSerpApiEventsTool(api: OpenClawPluginApi, ctx?: SerpApiToo hl: readStringParam(args, "hl") ?? undefined, start: readNumberParam(args, "start", { integer: true }) ?? undefined, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/facebook-profile.ts b/src/tools/facebook-profile.ts index 651216f..4b80b45 100644 --- a/src/tools/facebook-profile.ts +++ b/src/tools/facebook-profile.ts @@ -1,8 +1,8 @@ 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 { readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["profile_id", "zero_trace"] as const; @@ -33,6 +33,11 @@ export function createSerpApiFacebookProfileTool(api: OpenClawPluginApi, ctx?: S "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.", }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["profile_id"], additionalProperties: false, @@ -46,9 +51,10 @@ export function createSerpApiFacebookProfileTool(api: OpenClawPluginApi, ctx?: S params: { profile_id: readStringParam(args, "profile_id", { required: true }), }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/finance.ts b/src/tools/finance.ts index 2b911de..04a20d8 100644 --- a/src/tools/finance.ts +++ b/src/tools/finance.ts @@ -1,8 +1,8 @@ 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 { readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["q", "hl", "window", "zero_trace"] as const; @@ -42,6 +42,11 @@ export function createSerpApiFinanceTool(api: OpenClawPluginApi, ctx?: SerpApiTo type: "string", description: "Time window (default: 1D). Options: 1D, 5D, 1M, 6M, YTD, 1Y, 5Y, MAX.", }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["query"], additionalProperties: false, @@ -56,9 +61,10 @@ export function createSerpApiFinanceTool(api: OpenClawPluginApi, ctx?: SerpApiTo q: readStringParam(args, "query", { required: true }), window: readStringParam(args, "window") ?? "1D", }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/flights.ts b/src/tools/flights.ts index af62491..04e9805 100644 --- a/src/tools/flights.ts +++ b/src/tools/flights.ts @@ -1,8 +1,8 @@ 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 { readNumberParam, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = [ "departure_id", @@ -53,6 +53,11 @@ export function createSerpApiFlightsTool(api: OpenClawPluginApi, ctx?: SerpApiTo }, currency: { type: "string", description: "Currency code (default: USD)." }, gl: { type: "string", description: "Country code (e.g. us, de, ua)." }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["departure_id", "arrival_id", "outbound_date"], additionalProperties: false, @@ -81,9 +86,10 @@ export function createSerpApiFlightsTool(api: OpenClawPluginApi, ctx?: SerpApiTo currency: readStringParam(args, "currency") ?? "USD", gl: readStringParam(args, "gl") ?? undefined, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/hotels.ts b/src/tools/hotels.ts index 1e6a1de..0bedf61 100644 --- a/src/tools/hotels.ts +++ b/src/tools/hotels.ts @@ -1,8 +1,8 @@ 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 { readNumberParam, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { readBooleanArg, resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readBooleanArg, readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = [ "q", @@ -103,6 +103,11 @@ export function createSerpApiHotelsTool(api: OpenClawPluginApi, ctx?: SerpApiToo description: "Set true to search vacation rentals instead of hotels.", }, next_page_token: { type: "string", description: "Token for next page of results." }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["query"], additionalProperties: false, @@ -136,9 +141,10 @@ export function createSerpApiHotelsTool(api: OpenClawPluginApi, ctx?: SerpApiToo vacation_rentals: vacationRentals !== undefined ? String(vacationRentals) : undefined, next_page_token: readStringParam(args, "next_page_token") ?? undefined, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/immersive-product.ts b/src/tools/immersive-product.ts index 6955c93..b0be8ed 100644 --- a/src/tools/immersive-product.ts +++ b/src/tools/immersive-product.ts @@ -1,8 +1,8 @@ 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 { readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { readBooleanArg, resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readBooleanArg, readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["page_token", "more_stores", "next_page_token", "zero_trace"] as const; @@ -40,6 +40,11 @@ export function createSerpApiImmersiveProductTool(api: OpenClawPluginApi, ctx?: description: "Token from stores_next_page_token in a previous response. Use to retrieve the next page of stores.", }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["page_token"], additionalProperties: false, @@ -56,9 +61,10 @@ export function createSerpApiImmersiveProductTool(api: OpenClawPluginApi, ctx?: more_stores: moreStores, next_page_token: readStringParam(args, "next_page_token") ?? undefined, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/instagram-profile.ts b/src/tools/instagram-profile.ts index 6151343..f51ce81 100644 --- a/src/tools/instagram-profile.ts +++ b/src/tools/instagram-profile.ts @@ -1,8 +1,8 @@ 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 { readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["profile_id", "next_page_token", "zero_trace"] as const; @@ -38,6 +38,11 @@ export function createSerpApiInstagramProfileTool(api: OpenClawPluginApi, ctx?: type: "string", description: "Pagination token from serpapi_pagination.next_page_token returned in a previous response.", }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["profile_id"], additionalProperties: false, @@ -52,9 +57,10 @@ export function createSerpApiInstagramProfileTool(api: OpenClawPluginApi, ctx?: profile_id: readStringParam(args, "profile_id", { required: true }), next_page_token: readStringParam(args, "next_page_token") ?? undefined, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/jobs.ts b/src/tools/jobs.ts index a4552e1..02600b9 100644 --- a/src/tools/jobs.ts +++ b/src/tools/jobs.ts @@ -1,8 +1,8 @@ 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 { readNumberParam, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["q", "gl", "hl", "location", "lrad", "uds", "next_page_token", "zero_trace"] as const; @@ -43,6 +43,11 @@ export function createSerpApiJobsTool(api: OpenClawPluginApi, ctx?: SerpApiToolC description: "Filter string from filters[].parameters.uds in a previous response.", }, next_page_token: { type: "string", description: "Token for next page of results." }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["query"], additionalProperties: false, @@ -61,9 +66,13 @@ export function createSerpApiJobsTool(api: OpenClawPluginApi, ctx?: SerpApiToolC uds: readStringParam(args, "uds") ?? undefined, next_page_token: readStringParam(args, "next_page_token") ?? undefined, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw, count)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw, count), { + markdownHeading: "Jobs Results", + limit: count, + }); }, }; } diff --git a/src/tools/lens.ts b/src/tools/lens.ts index 7900026..d448681 100644 --- a/src/tools/lens.ts +++ b/src/tools/lens.ts @@ -1,8 +1,8 @@ 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 { readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { readBooleanArg, resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readBooleanArg, readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["url", "hl", "country", "type", "q", "safe", "auto_crop", "zero_trace"] as const; @@ -61,6 +61,11 @@ export function createSerpApiLensTool(api: OpenClawPluginApi, ctx?: SerpApiToolC description: "Whether Google auto-crops the image to focus on the detected area of interest (default: false). Not applicable for type=about_this_image.", }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["url"], additionalProperties: false, @@ -80,9 +85,10 @@ export function createSerpApiLensTool(api: OpenClawPluginApi, ctx?: SerpApiToolC safe: readStringParam(args, "safe") ?? undefined, auto_crop: readBooleanArg(args, "auto_crop") === true ? "true" : undefined, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/maps-reviews.ts b/src/tools/maps-reviews.ts index c13a233..81096a0 100644 --- a/src/tools/maps-reviews.ts +++ b/src/tools/maps-reviews.ts @@ -1,8 +1,8 @@ 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 { readNumberParam, readStringParam, wrapWebContent } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = [ "data_id", @@ -88,6 +88,11 @@ export function createSerpApiMapsReviewsTool(api: OpenClawPluginApi, ctx?: SerpA type: "string", description: "Pagination token from serpapi_pagination.next_page_token to fetch the next page.", }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: [], additionalProperties: false, @@ -130,9 +135,10 @@ export function createSerpApiMapsReviewsTool(api: OpenClawPluginApi, ctx?: SerpA num: resolvedNum, next_page_token: nextPageToken ?? undefined, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/maps.ts b/src/tools/maps.ts index e25f6f9..b047c7f 100644 --- a/src/tools/maps.ts +++ b/src/tools/maps.ts @@ -1,8 +1,8 @@ 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 { readNumberParam, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["q", "gl", "hl", "ll", "location", "z", "type", "nearby", "start", "zero_trace"] as const; @@ -53,6 +53,11 @@ export function createSerpApiMapsTool(api: OpenClawPluginApi, ctx?: SerpApiToolC description: "Force results near this location. Recommended when query contains 'near me'.", }, start: { type: "number", description: "Result offset for pagination (0, 20, 40...)." }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["query"], additionalProperties: false, @@ -82,9 +87,13 @@ export function createSerpApiMapsTool(api: OpenClawPluginApi, ctx?: SerpApiToolC nearby: readStringParam(args, "nearby") ?? undefined, start: readNumberParam(args, "start", { integer: true }) ?? undefined, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw, count)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw, count), { + markdownHeading: "Local Results", + limit: count, + }); }, }; } diff --git a/src/tools/news.ts b/src/tools/news.ts index f68e5a8..a495cba 100644 --- a/src/tools/news.ts +++ b/src/tools/news.ts @@ -1,8 +1,8 @@ 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 { readNumberParam, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = [ "q", @@ -72,6 +72,11 @@ export function createSerpApiNewsTool(api: OpenClawPluginApi, ctx?: SerpApiToolC type: "string", description: "Story token for full coverage of a specific story.", }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: [], additionalProperties: false, @@ -109,9 +114,13 @@ export function createSerpApiNewsTool(api: OpenClawPluginApi, ctx?: SerpApiToolC section_token: sectionToken ?? undefined, story_token: storyToken ?? undefined, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw, count)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw, count), { + markdownHeading: "News Results", + limit: count, + }); }, }; } diff --git a/src/tools/scholar.ts b/src/tools/scholar.ts index 773cfc0..d83eece 100644 --- a/src/tools/scholar.ts +++ b/src/tools/scholar.ts @@ -1,8 +1,8 @@ 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 { readNumberParam, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = [ "q", @@ -72,6 +72,11 @@ export function createSerpApiScholarTool(api: OpenClawPluginApi, ctx?: SerpApiTo description: 'Language restriction, pipe-separated (e.g. "lang_en|lang_de").', }, start: { type: "number", description: "Result offset for pagination (0, 10, 20...)." }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: [], additionalProperties: false, @@ -104,9 +109,13 @@ export function createSerpApiScholarTool(api: OpenClawPluginApi, ctx?: SerpApiTo start: readNumberParam(args, "start", { integer: true }) ?? undefined, num: count, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw, count)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw, count), { + markdownHeading: "Scholar Results", + limit: count, + }); }, }; } diff --git a/src/tools/shopping.ts b/src/tools/shopping.ts index 2e33625..96276fd 100644 --- a/src/tools/shopping.ts +++ b/src/tools/shopping.ts @@ -1,8 +1,8 @@ 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 { readNumberParam, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { readBooleanArg, resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readBooleanArg, readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = [ "q", @@ -60,6 +60,11 @@ export function createSerpApiShoppingTool(api: OpenClawPluginApi, ctx?: SerpApiT description: "Filter token from filters[].options[].shoprs in a previous response.", }, start: { type: "number", description: "Result offset for pagination (0, 60, 120...)." }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["query"], additionalProperties: false, @@ -84,9 +89,13 @@ export function createSerpApiShoppingTool(api: OpenClawPluginApi, ctx?: SerpApiT shoprs: readStringParam(args, "shoprs") ?? undefined, start: readNumberParam(args, "start", { integer: true }) ?? undefined, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw, count)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw, count), { + markdownHeading: "Shopping Results", + limit: count, + }); }, }; } diff --git a/src/tools/trends.ts b/src/tools/trends.ts index 3775007..0cf005c 100644 --- a/src/tools/trends.ts +++ b/src/tools/trends.ts @@ -1,8 +1,8 @@ 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 { readNumberParam, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["q", "geo", "date", "data_type", "hl", "cat", "gprop", "region", "tz", "zero_trace"] as const; @@ -59,6 +59,11 @@ export function createSerpApiTrendsTool(api: OpenClawPluginApi, ctx?: SerpApiToo type: "number", description: "Timezone offset in minutes (e.g. -540 for Tokyo, 420 for PDT). Default: 420.", }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["query"], additionalProperties: false, @@ -79,9 +84,10 @@ export function createSerpApiTrendsTool(api: OpenClawPluginApi, ctx?: SerpApiToo gprop: readStringParam(args, "gprop") ?? undefined, tz: readNumberParam(args, "tz", { integer: true }) ?? 420, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/tripadvisor.ts b/src/tools/tripadvisor.ts index 98f2683..04c5059 100644 --- a/src/tools/tripadvisor.ts +++ b/src/tools/tripadvisor.ts @@ -1,8 +1,8 @@ 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 { readNumberParam, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["q", "lat", "lon", "tripadvisor_domain", "ssrc", "offset", "limit", "zero_trace"] as const; @@ -60,6 +60,11 @@ export function createSerpApiTripadvisorTool(api: OpenClawPluginApi, ctx?: SerpA description: "Result offset for pagination (default: 0; use 30 for page 2, 60 for page 3, ...).", minimum: 0, }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["query"], additionalProperties: false, @@ -79,9 +84,10 @@ export function createSerpApiTripadvisorTool(api: OpenClawPluginApi, ctx?: SerpA limit: readNumberParam(args, "limit", { integer: true }) ?? 30, offset: readNumberParam(args, "offset", { integer: true }) ?? undefined, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/walmart-product.ts b/src/tools/walmart-product.ts index d462994..5d03a6c 100644 --- a/src/tools/walmart-product.ts +++ b/src/tools/walmart-product.ts @@ -1,8 +1,8 @@ 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 { readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["product_id", "store_id", "zero_trace"] as const; @@ -34,6 +34,11 @@ export function createSerpApiWalmartProductTool(api: OpenClawPluginApi, ctx?: Se type: "string", description: "Store ID to get pricing for a specific Walmart store location.", }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["product_id"], additionalProperties: false, @@ -48,9 +53,10 @@ export function createSerpApiWalmartProductTool(api: OpenClawPluginApi, ctx?: Se product_id: readStringParam(args, "product_id", { required: true }), store_id: readStringParam(args, "store_id") ?? undefined, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/walmart.ts b/src/tools/walmart.ts index 6e1ba7c..2893646 100644 --- a/src/tools/walmart.ts +++ b/src/tools/walmart.ts @@ -1,8 +1,8 @@ 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 { readNumberParam, readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = [ "query", @@ -78,6 +78,11 @@ export function createSerpApiWalmartTool(api: OpenClawPluginApi, ctx?: SerpApiTo minimum: 1, maximum: 100, }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: [], additionalProperties: false, @@ -104,9 +109,10 @@ export function createSerpApiWalmartTool(api: OpenClawPluginApi, ctx?: SerpApiTo max_price: readNumberParam(args, "max_price") ?? undefined, page: readNumberParam(args, "page", { integer: true }) ?? 1, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/weather.ts b/src/tools/weather.ts index 14cbc22..e5f0270 100644 --- a/src/tools/weather.ts +++ b/src/tools/weather.ts @@ -1,8 +1,8 @@ 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 { readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["q", "gl", "hl", "google_domain", "zero_trace"] as const; @@ -38,6 +38,11 @@ export function createSerpApiWeatherTool(api: OpenClawPluginApi, ctx?: SerpApiTo type: "string", description: "Two-letter language code for the response (e.g. en, de, uk).", }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["query"], additionalProperties: false, @@ -53,9 +58,10 @@ export function createSerpApiWeatherTool(api: OpenClawPluginApi, ctx?: SerpApiTo gl: readStringParam(args, "gl") ?? undefined, hl: readStringParam(args, "hl") ?? undefined, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/yahoo.ts b/src/tools/yahoo.ts index c13caa9..75290a8 100644 --- a/src/tools/yahoo.ts +++ b/src/tools/yahoo.ts @@ -1,8 +1,8 @@ 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 { readNumberParam, readStringParam, wrapWebContent } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["p", "yahoo_domain", "vc", "vl", "b", "vm", "vs", "vf", "zero_trace"] as const; @@ -69,6 +69,11 @@ export function createSerpApiYahooTool(api: OpenClawPluginApi, ctx?: SerpApiTool description: "Result offset for pagination (default: 1; use 11 for page 2, 21 for page 3, ...).", minimum: 1, }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["query"], additionalProperties: false, @@ -89,9 +94,10 @@ export function createSerpApiYahooTool(api: OpenClawPluginApi, ctx?: SerpApiTool vf: readStringParam(args, "vf") ?? undefined, b: readNumberParam(args, "b", { integer: true }) ?? 1, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/youtube-transcript.ts b/src/tools/youtube-transcript.ts index 4f42ba4..e88bccb 100644 --- a/src/tools/youtube-transcript.ts +++ b/src/tools/youtube-transcript.ts @@ -1,8 +1,8 @@ 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 { readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["v", "language_code", "title", "type", "zero_trace"] as const; @@ -46,6 +46,11 @@ export function createSerpApiYouTubeTranscriptTool(api: OpenClawPluginApi, ctx?: description: "Transcript type filter. Use 'asr' for auto-generated (automatic speech recognition) transcripts.", }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["v"], additionalProperties: false, @@ -62,9 +67,10 @@ export function createSerpApiYouTubeTranscriptTool(api: OpenClawPluginApi, ctx?: title: readStringParam(args, "title") ?? undefined, type: readStringParam(args, "type") ?? undefined, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/youtube-video.ts b/src/tools/youtube-video.ts index 72bbccc..c7b8390 100644 --- a/src/tools/youtube-video.ts +++ b/src/tools/youtube-video.ts @@ -1,8 +1,8 @@ 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 { readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["v", "gl", "hl", "next_page_token", "zero_trace"] as const; @@ -55,6 +55,11 @@ export function createSerpApiYouTubeVideoTool(api: OpenClawPluginApi, ctx?: Serp "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.", }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["v"], additionalProperties: false, @@ -71,9 +76,10 @@ export function createSerpApiYouTubeVideoTool(api: OpenClawPluginApi, ctx?: Serp gl: readStringParam(args, "gl") ?? undefined, next_page_token: readStringParam(args, "next_page_token") ?? undefined, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/tools/youtube.ts b/src/tools/youtube.ts index 0f457bc..2e664d2 100644 --- a/src/tools/youtube.ts +++ b/src/tools/youtube.ts @@ -1,8 +1,8 @@ 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 { readStringParam } from "openclaw/plugin-sdk/provider-web-search"; import { callSerpApi } from "../serpapi-client.js"; -import { resolveToolConfig, type SerpApiToolCtx } from "../utils.js"; +import { readOutputArg, resolveToolConfig, type SerpApiToolCtx, serpApiResult } from "../utils.js"; const ALLOWED_PARAMS = ["search_query", "hl", "sp", "zero_trace"] as const; @@ -30,6 +30,11 @@ export function createSerpApiYouTubeTool(api: OpenClawPluginApi, ctx?: SerpApiTo type: "string", description: 'YouTube search filters (e.g. "EgQIBBAB" for this week). Advanced use only.', }, + output: { + type: "string", + enum: ["md", "json"], + description: "Response format: 'md' (markdown, default, fewer tokens) or 'json' (structured).", + }, }, required: ["query"], additionalProperties: false, @@ -44,9 +49,10 @@ export function createSerpApiYouTubeTool(api: OpenClawPluginApi, ctx?: SerpApiTo search_query: readStringParam(args, "query", { required: true }), sp: readStringParam(args, "sp") ?? undefined, }, + output: readOutputArg(args), signal, }); - return jsonResult(extract(raw)); + return serpApiResult(typeof raw === "string" ? raw : extract(raw)); }, }; } diff --git a/src/utils.ts b/src/utils.ts index df71bad..0d9467c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,9 @@ 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"; +import { jsonResult, wrapWebContent } from "openclaw/plugin-sdk/provider-web-search"; +import type { SerpApiOutputFormat } from "./config.js"; +import { limitResultTable } from "./markdown.js"; export type SerpApiToolCtx = Pick; @@ -15,3 +18,31 @@ export function readBooleanArg(args: Record, key: string): bool if (v === false || v === "false") return false; return undefined; } + +export function readOutputArg(args: Record): SerpApiOutputFormat | undefined { + const v = args.output; + if (v === undefined || v === null || v === "") return undefined; + const s = String(v).trim().toLowerCase(); + if (s === "md" || s === "json") return s; + throw new Error(`output must be "md" or "json", got "${s}"`); +} + +export type SerpApiResultOptions = { + markdownHeading?: string; + limit?: number; +}; + +/** wrapWebContent is a security boundary: markdown is untrusted SERP content. */ +export function serpApiResult( + response: Record | string, + opts?: SerpApiResultOptions, +): ReturnType { + if (typeof response === "string") { + let markdown = response; + if (opts?.markdownHeading && opts.limit !== undefined) { + markdown = limitResultTable(markdown, { heading: opts.markdownHeading, limit: opts.limit }); + } + return jsonResult({ markdown: wrapWebContent(markdown, "web_search") }); + } + return jsonResult(response); +}