@openclaw/brave-plugin 2026.5.2 → 2026.5.3-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,114 @@
1
+ import { isDiagnosticFlagEnabled } from "openclaw/plugin-sdk/diagnostic-runtime";
2
+ import { createWebSearchProviderContractFields } from "openclaw/plugin-sdk/provider-web-search-config-contract";
3
+ //#region extensions/brave/src/brave-web-search-provider.ts
4
+ const BRAVE_CREDENTIAL_PATH = "plugins.entries.brave.config.webSearch.apiKey";
5
+ let braveWebSearchRuntimePromise;
6
+ function loadBraveWebSearchRuntime() {
7
+ braveWebSearchRuntimePromise ??= import("./brave-web-search-provider.runtime-Zj1jGbhx.js");
8
+ return braveWebSearchRuntimePromise;
9
+ }
10
+ const BraveSearchSchema = {
11
+ type: "object",
12
+ properties: {
13
+ query: {
14
+ type: "string",
15
+ description: "Search query string."
16
+ },
17
+ count: {
18
+ type: "number",
19
+ description: "Number of results to return (1-10).",
20
+ minimum: 1,
21
+ maximum: 10
22
+ },
23
+ country: {
24
+ type: "string",
25
+ description: "2-letter country code for region-specific results (e.g., 'DE', 'US', 'ALL'). Default: 'US'."
26
+ },
27
+ language: {
28
+ type: "string",
29
+ description: "ISO 639-1 language code for results (e.g., 'en', 'de', 'fr')."
30
+ },
31
+ freshness: {
32
+ type: "string",
33
+ description: "Filter by time: 'day' (24h), 'week', 'month', or 'year'."
34
+ },
35
+ date_after: {
36
+ type: "string",
37
+ description: "Only results published after this date (YYYY-MM-DD)."
38
+ },
39
+ date_before: {
40
+ type: "string",
41
+ description: "Only results published before this date (YYYY-MM-DD)."
42
+ },
43
+ search_lang: {
44
+ type: "string",
45
+ description: "Brave language code for search results (e.g., 'en', 'de', 'en-gb', 'zh-hans', 'zh-hant', 'pt-br')."
46
+ },
47
+ ui_lang: {
48
+ type: "string",
49
+ description: "Locale code for UI elements in language-region format (e.g., 'en-US', 'de-DE', 'fr-FR', 'tr-TR'). Must include region subtag."
50
+ }
51
+ }
52
+ };
53
+ function isRecord(value) {
54
+ return typeof value === "object" && value !== null && !Array.isArray(value);
55
+ }
56
+ function resolveProviderWebSearchPluginConfig(config, pluginId) {
57
+ if (!isRecord(config)) return;
58
+ const plugins = isRecord(config.plugins) ? config.plugins : void 0;
59
+ const entries = isRecord(plugins?.entries) ? plugins.entries : void 0;
60
+ const entry = isRecord(entries?.[pluginId]) ? entries[pluginId] : void 0;
61
+ const pluginConfig = isRecord(entry?.config) ? entry.config : void 0;
62
+ return isRecord(pluginConfig?.webSearch) ? pluginConfig.webSearch : void 0;
63
+ }
64
+ function mergeScopedSearchConfig(searchConfig, key, pluginConfig, options) {
65
+ if (!pluginConfig) return searchConfig;
66
+ const currentScoped = isRecord(searchConfig?.[key]) ? searchConfig?.[key] : {};
67
+ const next = {
68
+ ...searchConfig,
69
+ [key]: {
70
+ ...currentScoped,
71
+ ...pluginConfig
72
+ }
73
+ };
74
+ if (options?.mirrorApiKeyToTopLevel && pluginConfig.apiKey !== void 0) next.apiKey = pluginConfig.apiKey;
75
+ return next;
76
+ }
77
+ function resolveBraveMode(searchConfig) {
78
+ return (isRecord(searchConfig?.brave) ? searchConfig.brave : void 0)?.mode === "llm-context" ? "llm-context" : "web";
79
+ }
80
+ function createBraveToolDefinition(searchConfig, config) {
81
+ const braveMode = resolveBraveMode(searchConfig);
82
+ const diagnosticsEnabled = isDiagnosticFlagEnabled("brave.http", config);
83
+ return {
84
+ description: braveMode === "llm-context" ? "Search the web using Brave Search LLM Context API. Returns pre-extracted page content (text chunks, tables, code blocks) optimized for LLM grounding." : "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.",
85
+ parameters: BraveSearchSchema,
86
+ execute: async (args) => {
87
+ const { executeBraveSearch } = await loadBraveWebSearchRuntime();
88
+ return await executeBraveSearch(args, searchConfig, { diagnosticsEnabled });
89
+ }
90
+ };
91
+ }
92
+ function createBraveWebSearchProvider() {
93
+ return {
94
+ id: "brave",
95
+ label: "Brave Search",
96
+ hint: "Structured results · country/language/time filters",
97
+ onboardingScopes: ["text-inference"],
98
+ credentialLabel: "Brave Search API key",
99
+ envVars: ["BRAVE_API_KEY"],
100
+ placeholder: "BSA...",
101
+ signupUrl: "https://brave.com/search/api/",
102
+ docsUrl: "https://docs.openclaw.ai/tools/brave-search",
103
+ autoDetectOrder: 10,
104
+ credentialPath: BRAVE_CREDENTIAL_PATH,
105
+ ...createWebSearchProviderContractFields({
106
+ credentialPath: BRAVE_CREDENTIAL_PATH,
107
+ searchCredential: { type: "top-level" },
108
+ configuredCredential: { pluginId: "brave" }
109
+ }),
110
+ createTool: (ctx) => createBraveToolDefinition(mergeScopedSearchConfig(ctx.searchConfig, "brave", resolveProviderWebSearchPluginConfig(ctx.config, "brave"), { mirrorApiKeyToTopLevel: true }), ctx.config)
111
+ };
112
+ }
113
+ //#endregion
114
+ export { createBraveWebSearchProvider as t };
@@ -0,0 +1,360 @@
1
+ import { a as resolveBraveMode, i as resolveBraveConfig, n as normalizeBraveCountry, r as normalizeBraveLanguageParams, t as mapBraveLlmContextResults } from "./brave-web-search-provider.shared-Dca5ya1G.js";
2
+ import { DEFAULT_SEARCH_COUNT, buildSearchCacheKey, formatCliCommand, normalizeFreshness, parseIsoDateRange, readCachedSearchPayload, readConfiguredSecretString, readNumberParam, readProviderEnvValue, readStringParam, resolveSearchCacheTtlMs, resolveSearchCount, resolveSearchTimeoutSeconds, resolveSiteName, withSelfHostedWebSearchEndpoint, withTrustedWebSearchEndpoint, wrapWebContent, writeCachedSearchPayload } from "openclaw/plugin-sdk/provider-web-search";
3
+ import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
4
+ import { assertHttpUrlTargetsPrivateNetwork, isBlockedHostnameOrIp, isPrivateIpAddress, resolvePinnedHostnameWithPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
5
+ //#region extensions/brave/src/brave-web-search-provider.runtime.ts
6
+ const DEFAULT_BRAVE_BASE_URL = "https://api.search.brave.com";
7
+ const BRAVE_SEARCH_ENDPOINT_PATH = "/res/v1/web/search";
8
+ const BRAVE_LLM_CONTEXT_ENDPOINT_PATH = "/res/v1/llm/context";
9
+ const braveHttpLogger = createSubsystemLogger("brave/http");
10
+ function logBraveHttp(diagnostics, event, meta) {
11
+ if (!diagnostics?.enabled) return;
12
+ braveHttpLogger.info(`brave http ${event}`, meta);
13
+ }
14
+ function describeBraveRequestUrl(url) {
15
+ return {
16
+ url: url.toString(),
17
+ query: url.searchParams.get("q") ?? "",
18
+ params: Object.fromEntries(url.searchParams.entries())
19
+ };
20
+ }
21
+ function resolveBraveApiKey(searchConfig) {
22
+ return readConfiguredSecretString(searchConfig?.apiKey, "tools.web.search.apiKey") ?? readProviderEnvValue(["BRAVE_API_KEY"]);
23
+ }
24
+ function resolveBraveBaseUrl(braveConfig) {
25
+ return readConfiguredSecretString(braveConfig?.baseUrl, "plugins.entries.brave.config.webSearch.baseUrl")?.replace(/\/+$/u, "") || DEFAULT_BRAVE_BASE_URL;
26
+ }
27
+ function buildBraveEndpointUrl(params) {
28
+ const url = new URL(params.baseUrl);
29
+ url.pathname = `${url.pathname.replace(/\/+$/u, "")}${params.endpointPath}`;
30
+ url.search = "";
31
+ return url;
32
+ }
33
+ async function braveEndpointTargetsPrivateNetwork(url) {
34
+ if (isBlockedHostnameOrIp(url.hostname)) return true;
35
+ try {
36
+ return (await resolvePinnedHostnameWithPolicy(url.hostname, { policy: {
37
+ allowPrivateNetwork: true,
38
+ allowRfc2544BenchmarkRange: true
39
+ } })).addresses.every((address) => isPrivateIpAddress(address));
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+ async function validateBraveBaseUrl(baseUrl) {
45
+ let parsed;
46
+ try {
47
+ parsed = new URL(baseUrl);
48
+ } catch {
49
+ throw new Error("Brave Search base URL must be a valid http:// or https:// URL.");
50
+ }
51
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("Brave Search base URL must use http:// or https://.");
52
+ if (parsed.protocol === "http:") {
53
+ await assertHttpUrlTargetsPrivateNetwork(parsed.toString(), {
54
+ dangerouslyAllowPrivateNetwork: true,
55
+ errorMessage: "Brave Search HTTP base URL must target a trusted private or loopback host. Use https:// for public hosts."
56
+ });
57
+ return "selfHosted";
58
+ }
59
+ return await braveEndpointTargetsPrivateNetwork(parsed) ? "selfHosted" : "strict";
60
+ }
61
+ function missingBraveKeyPayload() {
62
+ return {
63
+ error: "missing_brave_api_key",
64
+ message: `web_search (brave) needs a Brave Search API key. Run \`${formatCliCommand("openclaw configure --section web")}\` to store it, or set BRAVE_API_KEY in the Gateway environment. If you do not want to configure a search API key, use web_fetch for a specific URL or the browser tool for interactive pages.`,
65
+ docs: "https://docs.openclaw.ai/tools/web"
66
+ };
67
+ }
68
+ async function runBraveLlmContextSearch(params) {
69
+ const url = buildBraveEndpointUrl({
70
+ baseUrl: params.baseUrl,
71
+ endpointPath: BRAVE_LLM_CONTEXT_ENDPOINT_PATH
72
+ });
73
+ url.searchParams.set("q", params.query);
74
+ if (params.country) url.searchParams.set("country", params.country);
75
+ if (params.search_lang) url.searchParams.set("search_lang", params.search_lang);
76
+ if (params.freshness) url.searchParams.set("freshness", params.freshness);
77
+ else if (params.dateAfter && params.dateBefore) url.searchParams.set("freshness", `${params.dateAfter}to${params.dateBefore}`);
78
+ else if (params.dateAfter) url.searchParams.set("freshness", `${params.dateAfter}to${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
79
+ logBraveHttp(params.diagnostics, "request", {
80
+ mode: "llm-context",
81
+ ...describeBraveRequestUrl(url)
82
+ });
83
+ const startedAt = Date.now();
84
+ return (params.endpointMode === "selfHosted" ? withSelfHostedWebSearchEndpoint : withTrustedWebSearchEndpoint)({
85
+ url: url.toString(),
86
+ timeoutSeconds: params.timeoutSeconds,
87
+ init: {
88
+ method: "GET",
89
+ headers: {
90
+ Accept: "application/json",
91
+ "X-Subscription-Token": params.apiKey
92
+ }
93
+ }
94
+ }, async (response) => {
95
+ logBraveHttp(params.diagnostics, "response", {
96
+ mode: "llm-context",
97
+ status: response.status,
98
+ ok: response.ok,
99
+ durationMs: Date.now() - startedAt
100
+ });
101
+ if (!response.ok) {
102
+ const detail = await response.text();
103
+ throw new Error(`Brave LLM Context API error (${response.status}): ${detail || response.statusText}`);
104
+ }
105
+ const data = await response.json();
106
+ return {
107
+ results: mapBraveLlmContextResults(data),
108
+ sources: data.sources
109
+ };
110
+ });
111
+ }
112
+ async function runBraveWebSearch(params) {
113
+ const url = buildBraveEndpointUrl({
114
+ baseUrl: params.baseUrl,
115
+ endpointPath: BRAVE_SEARCH_ENDPOINT_PATH
116
+ });
117
+ url.searchParams.set("q", params.query);
118
+ url.searchParams.set("count", String(params.count));
119
+ if (params.country) url.searchParams.set("country", params.country);
120
+ if (params.search_lang) url.searchParams.set("search_lang", params.search_lang);
121
+ if (params.ui_lang) url.searchParams.set("ui_lang", params.ui_lang);
122
+ if (params.freshness) url.searchParams.set("freshness", params.freshness);
123
+ else if (params.dateAfter && params.dateBefore) url.searchParams.set("freshness", `${params.dateAfter}to${params.dateBefore}`);
124
+ else if (params.dateAfter) url.searchParams.set("freshness", `${params.dateAfter}to${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
125
+ else if (params.dateBefore) url.searchParams.set("freshness", `1970-01-01to${params.dateBefore}`);
126
+ logBraveHttp(params.diagnostics, "request", {
127
+ mode: "web",
128
+ ...describeBraveRequestUrl(url)
129
+ });
130
+ const startedAt = Date.now();
131
+ return (params.endpointMode === "selfHosted" ? withSelfHostedWebSearchEndpoint : withTrustedWebSearchEndpoint)({
132
+ url: url.toString(),
133
+ timeoutSeconds: params.timeoutSeconds,
134
+ init: {
135
+ method: "GET",
136
+ headers: {
137
+ Accept: "application/json",
138
+ "X-Subscription-Token": params.apiKey
139
+ }
140
+ }
141
+ }, async (response) => {
142
+ logBraveHttp(params.diagnostics, "response", {
143
+ mode: "web",
144
+ status: response.status,
145
+ ok: response.ok,
146
+ durationMs: Date.now() - startedAt
147
+ });
148
+ if (!response.ok) {
149
+ const detail = await response.text();
150
+ throw new Error(`Brave Search API error (${response.status}): ${detail || response.statusText}`);
151
+ }
152
+ const data = await response.json();
153
+ return (Array.isArray(data.web?.results) ? data.web?.results ?? [] : []).map((entry) => {
154
+ const description = entry.description ?? "";
155
+ const title = entry.title ?? "";
156
+ const url = entry.url ?? "";
157
+ return {
158
+ title: title ? wrapWebContent(title, "web_search") : "",
159
+ url,
160
+ description: description ? wrapWebContent(description, "web_search") : "",
161
+ published: entry.age || void 0,
162
+ siteName: resolveSiteName(url) || void 0
163
+ };
164
+ });
165
+ });
166
+ }
167
+ async function executeBraveSearch(args, searchConfig, options) {
168
+ const apiKey = resolveBraveApiKey(searchConfig);
169
+ if (!apiKey) return missingBraveKeyPayload();
170
+ const braveConfig = resolveBraveConfig(searchConfig);
171
+ const braveMode = resolveBraveMode(braveConfig);
172
+ const braveBaseUrl = resolveBraveBaseUrl(braveConfig);
173
+ const braveEndpointMode = await validateBraveBaseUrl(braveBaseUrl);
174
+ const query = readStringParam(args, "query", { required: true });
175
+ const count = readNumberParam(args, "count", { integer: true }) ?? searchConfig?.maxResults ?? void 0;
176
+ const country = normalizeBraveCountry(readStringParam(args, "country"));
177
+ const language = readStringParam(args, "language");
178
+ const search_lang = readStringParam(args, "search_lang");
179
+ const ui_lang = readStringParam(args, "ui_lang");
180
+ const normalizedLanguage = normalizeBraveLanguageParams({
181
+ search_lang: search_lang || language,
182
+ ui_lang
183
+ });
184
+ if (normalizedLanguage.invalidField === "search_lang") return {
185
+ error: "invalid_search_lang",
186
+ message: "search_lang must be a Brave-supported language code like 'en', 'en-gb', 'zh-hans', or 'zh-hant'.",
187
+ docs: "https://docs.openclaw.ai/tools/web"
188
+ };
189
+ if (normalizedLanguage.invalidField === "ui_lang") return {
190
+ error: "invalid_ui_lang",
191
+ message: "ui_lang must be a language-region locale like 'en-US'.",
192
+ docs: "https://docs.openclaw.ai/tools/web"
193
+ };
194
+ if (normalizedLanguage.ui_lang && braveMode === "llm-context") return {
195
+ error: "unsupported_ui_lang",
196
+ message: "ui_lang is not supported by Brave llm-context mode. Remove ui_lang or use Brave web mode for locale-based UI hints.",
197
+ docs: "https://docs.openclaw.ai/tools/web"
198
+ };
199
+ const rawFreshness = readStringParam(args, "freshness");
200
+ const freshness = rawFreshness ? normalizeFreshness(rawFreshness, "brave") : void 0;
201
+ if (rawFreshness && !freshness) return {
202
+ error: "invalid_freshness",
203
+ message: "freshness must be day, week, month, or year.",
204
+ docs: "https://docs.openclaw.ai/tools/web"
205
+ };
206
+ const rawDateAfter = readStringParam(args, "date_after");
207
+ const rawDateBefore = readStringParam(args, "date_before");
208
+ if (rawFreshness && (rawDateAfter || rawDateBefore)) return {
209
+ error: "conflicting_time_filters",
210
+ message: "freshness and date_after/date_before cannot be used together. Use either freshness (day/week/month/year) or a date range (date_after/date_before), not both.",
211
+ docs: "https://docs.openclaw.ai/tools/web"
212
+ };
213
+ const parsedDateRange = parseIsoDateRange({
214
+ rawDateAfter,
215
+ rawDateBefore,
216
+ invalidDateAfterMessage: "date_after must be YYYY-MM-DD format.",
217
+ invalidDateBeforeMessage: "date_before must be YYYY-MM-DD format.",
218
+ invalidDateRangeMessage: "date_after must be before date_before."
219
+ });
220
+ if ("error" in parsedDateRange) return parsedDateRange;
221
+ const { dateAfter, dateBefore } = parsedDateRange;
222
+ if (braveMode === "llm-context") {
223
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
224
+ if (dateAfter && !dateBefore && dateAfter > today) return {
225
+ error: "invalid_date_range",
226
+ message: "date_after cannot be in the future for Brave llm-context mode.",
227
+ docs: "https://docs.openclaw.ai/tools/web"
228
+ };
229
+ if (dateBefore && !dateAfter) return {
230
+ error: "unsupported_date_filter",
231
+ message: "Brave llm-context mode requires date_after when date_before is set. Use a bounded date range or freshness.",
232
+ docs: "https://docs.openclaw.ai/tools/web"
233
+ };
234
+ }
235
+ const llmContextDateEnd = braveMode === "llm-context" && dateAfter ? dateBefore ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) : dateBefore;
236
+ const cacheKey = buildSearchCacheKey(braveMode === "llm-context" ? [
237
+ "brave",
238
+ braveMode,
239
+ braveBaseUrl,
240
+ query,
241
+ country,
242
+ normalizedLanguage.search_lang,
243
+ freshness,
244
+ dateAfter,
245
+ llmContextDateEnd
246
+ ] : [
247
+ "brave",
248
+ braveMode,
249
+ braveBaseUrl,
250
+ query,
251
+ resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
252
+ country,
253
+ normalizedLanguage.search_lang,
254
+ normalizedLanguage.ui_lang,
255
+ freshness,
256
+ dateAfter,
257
+ dateBefore
258
+ ]);
259
+ const diagnostics = { enabled: options?.diagnosticsEnabled === true };
260
+ const cached = readCachedSearchPayload(cacheKey);
261
+ if (cached) {
262
+ logBraveHttp(diagnostics, "cache hit", {
263
+ mode: braveMode,
264
+ query,
265
+ cacheKey
266
+ });
267
+ return cached;
268
+ }
269
+ logBraveHttp(diagnostics, "cache miss", {
270
+ mode: braveMode,
271
+ query,
272
+ cacheKey
273
+ });
274
+ const start = Date.now();
275
+ const timeoutSeconds = resolveSearchTimeoutSeconds(searchConfig);
276
+ const cacheTtlMs = resolveSearchCacheTtlMs(searchConfig);
277
+ if (braveMode === "llm-context") {
278
+ const { results, sources } = await runBraveLlmContextSearch({
279
+ baseUrl: braveBaseUrl,
280
+ endpointMode: braveEndpointMode,
281
+ query,
282
+ apiKey,
283
+ timeoutSeconds,
284
+ diagnostics,
285
+ country: country ?? void 0,
286
+ search_lang: normalizedLanguage.search_lang,
287
+ freshness,
288
+ dateAfter,
289
+ dateBefore
290
+ });
291
+ const payload = {
292
+ query,
293
+ provider: "brave",
294
+ mode: "llm-context",
295
+ count: results.length,
296
+ tookMs: Date.now() - start,
297
+ externalContent: {
298
+ untrusted: true,
299
+ source: "web_search",
300
+ provider: "brave",
301
+ wrapped: true
302
+ },
303
+ results: results.map((entry) => ({
304
+ title: entry.title ? wrapWebContent(entry.title, "web_search") : "",
305
+ url: entry.url,
306
+ snippets: entry.snippets.map((snippet) => wrapWebContent(snippet, "web_search")),
307
+ siteName: entry.siteName
308
+ })),
309
+ sources
310
+ };
311
+ writeCachedSearchPayload(cacheKey, payload, cacheTtlMs);
312
+ logBraveHttp(diagnostics, "cache write", {
313
+ mode: "llm-context",
314
+ query,
315
+ cacheKey,
316
+ ttlMs: cacheTtlMs,
317
+ count: results.length
318
+ });
319
+ return payload;
320
+ }
321
+ const results = await runBraveWebSearch({
322
+ baseUrl: braveBaseUrl,
323
+ endpointMode: braveEndpointMode,
324
+ query,
325
+ count: resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
326
+ apiKey,
327
+ timeoutSeconds,
328
+ diagnostics,
329
+ country: country ?? void 0,
330
+ search_lang: normalizedLanguage.search_lang,
331
+ ui_lang: normalizedLanguage.ui_lang,
332
+ freshness,
333
+ dateAfter,
334
+ dateBefore
335
+ });
336
+ const payload = {
337
+ query,
338
+ provider: "brave",
339
+ count: results.length,
340
+ tookMs: Date.now() - start,
341
+ externalContent: {
342
+ untrusted: true,
343
+ source: "web_search",
344
+ provider: "brave",
345
+ wrapped: true
346
+ },
347
+ results
348
+ };
349
+ writeCachedSearchPayload(cacheKey, payload, cacheTtlMs);
350
+ logBraveHttp(diagnostics, "cache write", {
351
+ mode: "web",
352
+ query,
353
+ cacheKey,
354
+ ttlMs: cacheTtlMs,
355
+ count: results.length
356
+ });
357
+ return payload;
358
+ }
359
+ //#endregion
360
+ export { executeBraveSearch };
@@ -0,0 +1,172 @@
1
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
2
+ //#region extensions/brave/src/brave-web-search-provider.shared.ts
3
+ const BRAVE_COUNTRY_CODES = new Set([
4
+ "AR",
5
+ "AU",
6
+ "AT",
7
+ "BE",
8
+ "BR",
9
+ "CA",
10
+ "CL",
11
+ "DK",
12
+ "FI",
13
+ "FR",
14
+ "DE",
15
+ "GR",
16
+ "HK",
17
+ "IN",
18
+ "ID",
19
+ "IT",
20
+ "JP",
21
+ "KR",
22
+ "MY",
23
+ "MX",
24
+ "NL",
25
+ "NZ",
26
+ "NO",
27
+ "CN",
28
+ "PL",
29
+ "PT",
30
+ "PH",
31
+ "RU",
32
+ "SA",
33
+ "ZA",
34
+ "ES",
35
+ "SE",
36
+ "CH",
37
+ "TW",
38
+ "TR",
39
+ "GB",
40
+ "US",
41
+ "ALL"
42
+ ]);
43
+ const BRAVE_SEARCH_LANG_CODES = new Set([
44
+ "ar",
45
+ "eu",
46
+ "bn",
47
+ "bg",
48
+ "ca",
49
+ "zh-hans",
50
+ "zh-hant",
51
+ "hr",
52
+ "cs",
53
+ "da",
54
+ "nl",
55
+ "en",
56
+ "en-gb",
57
+ "et",
58
+ "fi",
59
+ "fr",
60
+ "gl",
61
+ "de",
62
+ "el",
63
+ "gu",
64
+ "he",
65
+ "hi",
66
+ "hu",
67
+ "is",
68
+ "it",
69
+ "jp",
70
+ "kn",
71
+ "ko",
72
+ "lv",
73
+ "lt",
74
+ "ms",
75
+ "ml",
76
+ "mr",
77
+ "nb",
78
+ "pl",
79
+ "pt-br",
80
+ "pt-pt",
81
+ "pa",
82
+ "ro",
83
+ "ru",
84
+ "sr",
85
+ "sk",
86
+ "sl",
87
+ "es",
88
+ "sv",
89
+ "ta",
90
+ "te",
91
+ "th",
92
+ "tr",
93
+ "uk",
94
+ "vi"
95
+ ]);
96
+ const BRAVE_SEARCH_LANG_ALIASES = {
97
+ ja: "jp",
98
+ zh: "zh-hans",
99
+ "zh-cn": "zh-hans",
100
+ "zh-hk": "zh-hant",
101
+ "zh-sg": "zh-hans",
102
+ "zh-tw": "zh-hant"
103
+ };
104
+ const BRAVE_UI_LANG_LOCALE = /^([a-z]{2})-([a-z]{2})$/i;
105
+ function normalizeBraveSearchLang(value) {
106
+ if (!value) return;
107
+ const trimmed = value.trim();
108
+ if (!trimmed) return;
109
+ const lower = normalizeLowercaseStringOrEmpty(trimmed);
110
+ const canonical = BRAVE_SEARCH_LANG_ALIASES[lower] ?? lower;
111
+ if (!BRAVE_SEARCH_LANG_CODES.has(canonical)) return;
112
+ return canonical;
113
+ }
114
+ function normalizeBraveCountry(value) {
115
+ if (!value) return;
116
+ const trimmed = value.trim();
117
+ if (!trimmed) return;
118
+ const canonical = trimmed.toUpperCase();
119
+ return BRAVE_COUNTRY_CODES.has(canonical) ? canonical : "ALL";
120
+ }
121
+ function normalizeBraveUiLang(value) {
122
+ if (!value) return;
123
+ const trimmed = value.trim();
124
+ if (!trimmed) return;
125
+ const match = trimmed.match(BRAVE_UI_LANG_LOCALE);
126
+ if (!match) return;
127
+ const [, language, region] = match;
128
+ return `${normalizeLowercaseStringOrEmpty(language)}-${region.toUpperCase()}`;
129
+ }
130
+ function resolveBraveConfig(searchConfig) {
131
+ const brave = searchConfig?.brave;
132
+ return brave && typeof brave === "object" && !Array.isArray(brave) ? brave : {};
133
+ }
134
+ function resolveBraveMode(brave) {
135
+ return brave?.mode === "llm-context" ? "llm-context" : "web";
136
+ }
137
+ function normalizeBraveLanguageParams(params) {
138
+ const rawSearchLang = normalizeOptionalString(params.search_lang);
139
+ const rawUiLang = normalizeOptionalString(params.ui_lang);
140
+ let searchLangCandidate = rawSearchLang;
141
+ let uiLangCandidate = rawUiLang;
142
+ if (normalizeBraveUiLang(rawSearchLang) && normalizeBraveSearchLang(rawUiLang)) {
143
+ searchLangCandidate = rawUiLang;
144
+ uiLangCandidate = rawSearchLang;
145
+ }
146
+ const search_lang = normalizeBraveSearchLang(searchLangCandidate);
147
+ if (searchLangCandidate && !search_lang) return { invalidField: "search_lang" };
148
+ const ui_lang = normalizeBraveUiLang(uiLangCandidate);
149
+ if (uiLangCandidate && !ui_lang) return { invalidField: "ui_lang" };
150
+ return {
151
+ search_lang,
152
+ ui_lang
153
+ };
154
+ }
155
+ function resolveSiteName(url) {
156
+ if (!url) return;
157
+ try {
158
+ return new URL(url).hostname;
159
+ } catch {
160
+ return;
161
+ }
162
+ }
163
+ function mapBraveLlmContextResults(data) {
164
+ return (Array.isArray(data.grounding?.generic) ? data.grounding.generic : []).map((entry) => ({
165
+ url: entry.url ?? "",
166
+ title: entry.title ?? "",
167
+ snippets: (entry.snippets ?? []).filter((snippet) => typeof snippet === "string" && snippet.length > 0),
168
+ siteName: resolveSiteName(entry.url) || void 0
169
+ }));
170
+ }
171
+ //#endregion
172
+ export { resolveBraveMode as a, resolveBraveConfig as i, normalizeBraveCountry as n, normalizeBraveLanguageParams as r, mapBraveLlmContextResults as t };
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ import { t as createBraveWebSearchProvider } from "./brave-web-search-provider-CGCUaRRN.js";
2
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
3
+ //#region extensions/brave/index.ts
4
+ var brave_default = definePluginEntry({
5
+ id: "brave",
6
+ name: "Brave Plugin",
7
+ description: "Bundled Brave plugin",
8
+ register(api) {
9
+ api.registerWebSearchProvider(createBraveWebSearchProvider());
10
+ }
11
+ });
12
+ //#endregion
13
+ export { brave_default as default };
@@ -0,0 +1,10 @@
1
+ import { a as resolveBraveMode, n as normalizeBraveCountry, r as normalizeBraveLanguageParams, t as mapBraveLlmContextResults } from "./brave-web-search-provider.shared-Dca5ya1G.js";
2
+ //#region extensions/brave/test-api.ts
3
+ const __testing = {
4
+ normalizeBraveCountry,
5
+ normalizeBraveLanguageParams,
6
+ resolveBraveMode,
7
+ mapBraveLlmContextResults
8
+ };
9
+ //#endregion
10
+ export { __testing };
@@ -0,0 +1,26 @@
1
+ import { createWebSearchProviderContractFields } from "openclaw/plugin-sdk/provider-web-search-config-contract";
2
+ //#region extensions/brave/web-search-contract-api.ts
3
+ function createBraveWebSearchProvider() {
4
+ const credentialPath = "plugins.entries.brave.config.webSearch.apiKey";
5
+ return {
6
+ id: "brave",
7
+ label: "Brave Search",
8
+ hint: "Structured results · country/language/time filters",
9
+ onboardingScopes: ["text-inference"],
10
+ credentialLabel: "Brave Search API key",
11
+ envVars: ["BRAVE_API_KEY"],
12
+ placeholder: "BSA...",
13
+ signupUrl: "https://brave.com/search/api/",
14
+ docsUrl: "https://docs.openclaw.ai/tools/brave-search",
15
+ autoDetectOrder: 10,
16
+ credentialPath,
17
+ ...createWebSearchProviderContractFields({
18
+ credentialPath,
19
+ searchCredential: { type: "top-level" },
20
+ configuredCredential: { pluginId: "brave" }
21
+ }),
22
+ createTool: () => null
23
+ };
24
+ }
25
+ //#endregion
26
+ export { createBraveWebSearchProvider };