@openclaw/brave-plugin 2026.5.1-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.
package/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
2
+ import { createBraveWebSearchProvider } from "./src/brave-web-search-provider.js";
3
+
4
+ export default definePluginEntry({
5
+ id: "brave",
6
+ name: "Brave Plugin",
7
+ description: "Bundled Brave plugin",
8
+ register(api) {
9
+ api.registerWebSearchProvider(createBraveWebSearchProvider());
10
+ },
11
+ });
@@ -0,0 +1,46 @@
1
+ {
2
+ "id": "brave",
3
+ "activation": {
4
+ "onStartup": false
5
+ },
6
+ "providerAuthEnvVars": {
7
+ "brave": ["BRAVE_API_KEY"]
8
+ },
9
+ "uiHints": {
10
+ "webSearch.apiKey": {
11
+ "label": "Brave Search API Key",
12
+ "help": "Brave Search API key (fallback: BRAVE_API_KEY env var).",
13
+ "sensitive": true,
14
+ "placeholder": "BSA..."
15
+ },
16
+ "webSearch.mode": {
17
+ "label": "Brave Search Mode",
18
+ "help": "Brave Search mode: web or llm-context."
19
+ }
20
+ },
21
+ "contracts": {
22
+ "webSearchProviders": ["brave"]
23
+ },
24
+ "configContracts": {
25
+ "compatibilityRuntimePaths": ["tools.web.search.apiKey"]
26
+ },
27
+ "configSchema": {
28
+ "type": "object",
29
+ "additionalProperties": false,
30
+ "properties": {
31
+ "webSearch": {
32
+ "type": "object",
33
+ "additionalProperties": false,
34
+ "properties": {
35
+ "apiKey": {
36
+ "type": ["string", "object"]
37
+ },
38
+ "mode": {
39
+ "type": "string",
40
+ "enum": ["web", "llm-context"]
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@openclaw/brave-plugin",
3
+ "version": "2026.5.1-beta.1",
4
+ "description": "OpenClaw Brave plugin",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/openclaw/openclaw"
8
+ },
9
+ "type": "module",
10
+ "devDependencies": {
11
+ "@openclaw/plugin-sdk": "workspace:*"
12
+ },
13
+ "openclaw": {
14
+ "extensions": [
15
+ "./index.ts"
16
+ ],
17
+ "install": {
18
+ "npmSpec": "@openclaw/brave-plugin",
19
+ "defaultChoice": "npm",
20
+ "minHostVersion": ">=2026.4.10"
21
+ },
22
+ "compat": {
23
+ "pluginApi": ">=2026.4.10"
24
+ },
25
+ "build": {
26
+ "openclawVersion": "2026.5.1-beta.1"
27
+ },
28
+ "release": {
29
+ "publishToClawHub": true,
30
+ "publishToNpm": true
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,367 @@
1
+ import type { SearchConfigRecord } from "openclaw/plugin-sdk/provider-web-search";
2
+ import {
3
+ buildSearchCacheKey,
4
+ DEFAULT_SEARCH_COUNT,
5
+ formatCliCommand,
6
+ normalizeFreshness,
7
+ parseIsoDateRange,
8
+ readCachedSearchPayload,
9
+ readConfiguredSecretString,
10
+ readNumberParam,
11
+ readProviderEnvValue,
12
+ readStringParam,
13
+ resolveSearchCacheTtlMs,
14
+ resolveSearchCount,
15
+ resolveSearchTimeoutSeconds,
16
+ resolveSiteName,
17
+ withTrustedWebSearchEndpoint,
18
+ wrapWebContent,
19
+ writeCachedSearchPayload,
20
+ } from "openclaw/plugin-sdk/provider-web-search";
21
+ import {
22
+ type BraveLlmContextResponse,
23
+ mapBraveLlmContextResults,
24
+ normalizeBraveCountry,
25
+ normalizeBraveLanguageParams,
26
+ resolveBraveConfig,
27
+ resolveBraveMode,
28
+ } from "./brave-web-search-provider.shared.js";
29
+
30
+ const BRAVE_SEARCH_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
31
+ const BRAVE_LLM_CONTEXT_ENDPOINT = "https://api.search.brave.com/res/v1/llm/context";
32
+
33
+ type BraveSearchResult = {
34
+ title?: string;
35
+ url?: string;
36
+ description?: string;
37
+ age?: string;
38
+ };
39
+
40
+ type BraveSearchResponse = {
41
+ web?: {
42
+ results?: BraveSearchResult[];
43
+ };
44
+ };
45
+
46
+ function resolveBraveApiKey(searchConfig?: SearchConfigRecord): string | undefined {
47
+ return (
48
+ readConfiguredSecretString(searchConfig?.apiKey, "tools.web.search.apiKey") ??
49
+ readProviderEnvValue(["BRAVE_API_KEY"])
50
+ );
51
+ }
52
+
53
+ function missingBraveKeyPayload() {
54
+ return {
55
+ error: "missing_brave_api_key",
56
+ 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.`,
57
+ docs: "https://docs.openclaw.ai/tools/web",
58
+ };
59
+ }
60
+
61
+ async function runBraveLlmContextSearch(params: {
62
+ query: string;
63
+ apiKey: string;
64
+ timeoutSeconds: number;
65
+ country?: string;
66
+ search_lang?: string;
67
+ freshness?: string;
68
+ }): Promise<{
69
+ results: Array<{
70
+ url: string;
71
+ title: string;
72
+ snippets: string[];
73
+ siteName?: string;
74
+ }>;
75
+ sources?: BraveLlmContextResponse["sources"];
76
+ }> {
77
+ const url = new URL(BRAVE_LLM_CONTEXT_ENDPOINT);
78
+ url.searchParams.set("q", params.query);
79
+ if (params.country) {
80
+ url.searchParams.set("country", params.country);
81
+ }
82
+ if (params.search_lang) {
83
+ url.searchParams.set("search_lang", params.search_lang);
84
+ }
85
+ if (params.freshness) {
86
+ url.searchParams.set("freshness", params.freshness);
87
+ }
88
+
89
+ return withTrustedWebSearchEndpoint(
90
+ {
91
+ url: url.toString(),
92
+ timeoutSeconds: params.timeoutSeconds,
93
+ init: {
94
+ method: "GET",
95
+ headers: {
96
+ Accept: "application/json",
97
+ "X-Subscription-Token": params.apiKey,
98
+ },
99
+ },
100
+ },
101
+ async (response) => {
102
+ if (!response.ok) {
103
+ const detail = await response.text();
104
+ throw new Error(
105
+ `Brave LLM Context API error (${response.status}): ${detail || response.statusText}`,
106
+ );
107
+ }
108
+
109
+ const data = (await response.json()) as BraveLlmContextResponse;
110
+ return { results: mapBraveLlmContextResults(data), sources: data.sources };
111
+ },
112
+ );
113
+ }
114
+
115
+ async function runBraveWebSearch(params: {
116
+ query: string;
117
+ count: number;
118
+ apiKey: string;
119
+ timeoutSeconds: number;
120
+ country?: string;
121
+ search_lang?: string;
122
+ ui_lang?: string;
123
+ freshness?: string;
124
+ dateAfter?: string;
125
+ dateBefore?: string;
126
+ }): Promise<Array<Record<string, unknown>>> {
127
+ const url = new URL(BRAVE_SEARCH_ENDPOINT);
128
+ url.searchParams.set("q", params.query);
129
+ url.searchParams.set("count", String(params.count));
130
+ if (params.country) {
131
+ url.searchParams.set("country", params.country);
132
+ }
133
+ if (params.search_lang) {
134
+ url.searchParams.set("search_lang", params.search_lang);
135
+ }
136
+ if (params.ui_lang) {
137
+ url.searchParams.set("ui_lang", params.ui_lang);
138
+ }
139
+ if (params.freshness) {
140
+ url.searchParams.set("freshness", params.freshness);
141
+ } else if (params.dateAfter && params.dateBefore) {
142
+ url.searchParams.set("freshness", `${params.dateAfter}to${params.dateBefore}`);
143
+ } else if (params.dateAfter) {
144
+ url.searchParams.set(
145
+ "freshness",
146
+ `${params.dateAfter}to${new Date().toISOString().slice(0, 10)}`,
147
+ );
148
+ } else if (params.dateBefore) {
149
+ url.searchParams.set("freshness", `1970-01-01to${params.dateBefore}`);
150
+ }
151
+
152
+ return withTrustedWebSearchEndpoint(
153
+ {
154
+ url: url.toString(),
155
+ timeoutSeconds: params.timeoutSeconds,
156
+ init: {
157
+ method: "GET",
158
+ headers: {
159
+ Accept: "application/json",
160
+ "X-Subscription-Token": params.apiKey,
161
+ },
162
+ },
163
+ },
164
+ async (response) => {
165
+ if (!response.ok) {
166
+ const detail = await response.text();
167
+ throw new Error(
168
+ `Brave Search API error (${response.status}): ${detail || response.statusText}`,
169
+ );
170
+ }
171
+
172
+ const data = (await response.json()) as BraveSearchResponse;
173
+ const results = Array.isArray(data.web?.results) ? (data.web?.results ?? []) : [];
174
+ return results.map((entry) => {
175
+ const description = entry.description ?? "";
176
+ const title = entry.title ?? "";
177
+ const url = entry.url ?? "";
178
+ return {
179
+ title: title ? wrapWebContent(title, "web_search") : "",
180
+ url,
181
+ description: description ? wrapWebContent(description, "web_search") : "",
182
+ published: entry.age || undefined,
183
+ siteName: resolveSiteName(url) || undefined,
184
+ };
185
+ });
186
+ },
187
+ );
188
+ }
189
+
190
+ export async function executeBraveSearch(
191
+ args: Record<string, unknown>,
192
+ searchConfig?: SearchConfigRecord,
193
+ ): Promise<Record<string, unknown>> {
194
+ const apiKey = resolveBraveApiKey(searchConfig);
195
+ if (!apiKey) {
196
+ return missingBraveKeyPayload();
197
+ }
198
+
199
+ const braveConfig = resolveBraveConfig(searchConfig);
200
+ const braveMode = resolveBraveMode(braveConfig);
201
+ const query = readStringParam(args, "query", { required: true });
202
+ const count =
203
+ readNumberParam(args, "count", { integer: true }) ?? searchConfig?.maxResults ?? undefined;
204
+ const country = normalizeBraveCountry(readStringParam(args, "country"));
205
+ const language = readStringParam(args, "language");
206
+ const search_lang = readStringParam(args, "search_lang");
207
+ const ui_lang = readStringParam(args, "ui_lang");
208
+ const normalizedLanguage = normalizeBraveLanguageParams({
209
+ search_lang: search_lang || language,
210
+ ui_lang,
211
+ });
212
+
213
+ if (normalizedLanguage.invalidField === "search_lang") {
214
+ return {
215
+ error: "invalid_search_lang",
216
+ message:
217
+ "search_lang must be a Brave-supported language code like 'en', 'en-gb', 'zh-hans', or 'zh-hant'.",
218
+ docs: "https://docs.openclaw.ai/tools/web",
219
+ };
220
+ }
221
+ if (normalizedLanguage.invalidField === "ui_lang") {
222
+ return {
223
+ error: "invalid_ui_lang",
224
+ message: "ui_lang must be a language-region locale like 'en-US'.",
225
+ docs: "https://docs.openclaw.ai/tools/web",
226
+ };
227
+ }
228
+ if (normalizedLanguage.ui_lang && braveMode === "llm-context") {
229
+ return {
230
+ error: "unsupported_ui_lang",
231
+ message:
232
+ "ui_lang is not supported by Brave llm-context mode. Remove ui_lang or use Brave web mode for locale-based UI hints.",
233
+ docs: "https://docs.openclaw.ai/tools/web",
234
+ };
235
+ }
236
+
237
+ const rawFreshness = readStringParam(args, "freshness");
238
+ if (rawFreshness && braveMode === "llm-context") {
239
+ return {
240
+ error: "unsupported_freshness",
241
+ message:
242
+ "freshness filtering is not supported by Brave llm-context mode. Remove freshness or use Brave web mode.",
243
+ docs: "https://docs.openclaw.ai/tools/web",
244
+ };
245
+ }
246
+ const freshness = rawFreshness ? normalizeFreshness(rawFreshness, "brave") : undefined;
247
+ if (rawFreshness && !freshness) {
248
+ return {
249
+ error: "invalid_freshness",
250
+ message: "freshness must be day, week, month, or year.",
251
+ docs: "https://docs.openclaw.ai/tools/web",
252
+ };
253
+ }
254
+
255
+ const rawDateAfter = readStringParam(args, "date_after");
256
+ const rawDateBefore = readStringParam(args, "date_before");
257
+ if (rawFreshness && (rawDateAfter || rawDateBefore)) {
258
+ return {
259
+ error: "conflicting_time_filters",
260
+ message:
261
+ "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.",
262
+ docs: "https://docs.openclaw.ai/tools/web",
263
+ };
264
+ }
265
+ if ((rawDateAfter || rawDateBefore) && braveMode === "llm-context") {
266
+ return {
267
+ error: "unsupported_date_filter",
268
+ message:
269
+ "date_after/date_before filtering is not supported by Brave llm-context mode. Use Brave web mode for date filters.",
270
+ docs: "https://docs.openclaw.ai/tools/web",
271
+ };
272
+ }
273
+
274
+ const parsedDateRange = parseIsoDateRange({
275
+ rawDateAfter,
276
+ rawDateBefore,
277
+ invalidDateAfterMessage: "date_after must be YYYY-MM-DD format.",
278
+ invalidDateBeforeMessage: "date_before must be YYYY-MM-DD format.",
279
+ invalidDateRangeMessage: "date_after must be before date_before.",
280
+ });
281
+ if ("error" in parsedDateRange) {
282
+ return parsedDateRange;
283
+ }
284
+
285
+ const { dateAfter, dateBefore } = parsedDateRange;
286
+ const cacheKey = buildSearchCacheKey([
287
+ "brave",
288
+ braveMode,
289
+ query,
290
+ resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
291
+ country,
292
+ normalizedLanguage.search_lang,
293
+ normalizedLanguage.ui_lang,
294
+ freshness,
295
+ dateAfter,
296
+ dateBefore,
297
+ ]);
298
+ const cached = readCachedSearchPayload(cacheKey);
299
+ if (cached) {
300
+ return cached;
301
+ }
302
+
303
+ const start = Date.now();
304
+ const timeoutSeconds = resolveSearchTimeoutSeconds(searchConfig);
305
+ const cacheTtlMs = resolveSearchCacheTtlMs(searchConfig);
306
+
307
+ if (braveMode === "llm-context") {
308
+ const { results, sources } = await runBraveLlmContextSearch({
309
+ query,
310
+ apiKey,
311
+ timeoutSeconds,
312
+ country: country ?? undefined,
313
+ search_lang: normalizedLanguage.search_lang,
314
+ freshness,
315
+ });
316
+ const payload = {
317
+ query,
318
+ provider: "brave",
319
+ mode: "llm-context" as const,
320
+ count: results.length,
321
+ tookMs: Date.now() - start,
322
+ externalContent: {
323
+ untrusted: true,
324
+ source: "web_search",
325
+ provider: "brave",
326
+ wrapped: true,
327
+ },
328
+ results: results.map((entry) => ({
329
+ title: entry.title ? wrapWebContent(entry.title, "web_search") : "",
330
+ url: entry.url,
331
+ snippets: entry.snippets.map((snippet) => wrapWebContent(snippet, "web_search")),
332
+ siteName: entry.siteName,
333
+ })),
334
+ sources,
335
+ };
336
+ writeCachedSearchPayload(cacheKey, payload, cacheTtlMs);
337
+ return payload;
338
+ }
339
+
340
+ const results = await runBraveWebSearch({
341
+ query,
342
+ count: resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
343
+ apiKey,
344
+ timeoutSeconds,
345
+ country: country ?? undefined,
346
+ search_lang: normalizedLanguage.search_lang,
347
+ ui_lang: normalizedLanguage.ui_lang,
348
+ freshness,
349
+ dateAfter,
350
+ dateBefore,
351
+ });
352
+ const payload = {
353
+ query,
354
+ provider: "brave",
355
+ count: results.length,
356
+ tookMs: Date.now() - start,
357
+ externalContent: {
358
+ untrusted: true,
359
+ source: "web_search",
360
+ provider: "brave",
361
+ wrapped: true,
362
+ },
363
+ results,
364
+ };
365
+ writeCachedSearchPayload(cacheKey, payload, cacheTtlMs);
366
+ return payload;
367
+ }
@@ -0,0 +1,226 @@
1
+ import {
2
+ normalizeLowercaseStringOrEmpty,
3
+ normalizeOptionalString,
4
+ } from "openclaw/plugin-sdk/text-runtime";
5
+
6
+ type BraveConfig = {
7
+ mode?: string;
8
+ };
9
+
10
+ type BraveLlmContextResult = { url: string; title: string; snippets: string[] };
11
+ export type BraveLlmContextResponse = {
12
+ grounding: { generic?: BraveLlmContextResult[] };
13
+ sources?: { url?: string; hostname?: string; date?: string }[];
14
+ };
15
+
16
+ const BRAVE_COUNTRY_CODES = new Set([
17
+ "AR",
18
+ "AU",
19
+ "AT",
20
+ "BE",
21
+ "BR",
22
+ "CA",
23
+ "CL",
24
+ "DK",
25
+ "FI",
26
+ "FR",
27
+ "DE",
28
+ "GR",
29
+ "HK",
30
+ "IN",
31
+ "ID",
32
+ "IT",
33
+ "JP",
34
+ "KR",
35
+ "MY",
36
+ "MX",
37
+ "NL",
38
+ "NZ",
39
+ "NO",
40
+ "CN",
41
+ "PL",
42
+ "PT",
43
+ "PH",
44
+ "RU",
45
+ "SA",
46
+ "ZA",
47
+ "ES",
48
+ "SE",
49
+ "CH",
50
+ "TW",
51
+ "TR",
52
+ "GB",
53
+ "US",
54
+ "ALL",
55
+ ]);
56
+
57
+ const BRAVE_SEARCH_LANG_CODES = new Set([
58
+ "ar",
59
+ "eu",
60
+ "bn",
61
+ "bg",
62
+ "ca",
63
+ "zh-hans",
64
+ "zh-hant",
65
+ "hr",
66
+ "cs",
67
+ "da",
68
+ "nl",
69
+ "en",
70
+ "en-gb",
71
+ "et",
72
+ "fi",
73
+ "fr",
74
+ "gl",
75
+ "de",
76
+ "el",
77
+ "gu",
78
+ "he",
79
+ "hi",
80
+ "hu",
81
+ "is",
82
+ "it",
83
+ "jp",
84
+ "kn",
85
+ "ko",
86
+ "lv",
87
+ "lt",
88
+ "ms",
89
+ "ml",
90
+ "mr",
91
+ "nb",
92
+ "pl",
93
+ "pt-br",
94
+ "pt-pt",
95
+ "pa",
96
+ "ro",
97
+ "ru",
98
+ "sr",
99
+ "sk",
100
+ "sl",
101
+ "es",
102
+ "sv",
103
+ "ta",
104
+ "te",
105
+ "th",
106
+ "tr",
107
+ "uk",
108
+ "vi",
109
+ ]);
110
+
111
+ const BRAVE_SEARCH_LANG_ALIASES: Record<string, string> = {
112
+ ja: "jp",
113
+ zh: "zh-hans",
114
+ "zh-cn": "zh-hans",
115
+ "zh-hk": "zh-hant",
116
+ "zh-sg": "zh-hans",
117
+ "zh-tw": "zh-hant",
118
+ };
119
+
120
+ const BRAVE_UI_LANG_LOCALE = /^([a-z]{2})-([a-z]{2})$/i;
121
+
122
+ function normalizeBraveSearchLang(value: string | undefined): string | undefined {
123
+ if (!value) {
124
+ return undefined;
125
+ }
126
+ const trimmed = value.trim();
127
+ if (!trimmed) {
128
+ return undefined;
129
+ }
130
+ const lower = normalizeLowercaseStringOrEmpty(trimmed);
131
+ const canonical = BRAVE_SEARCH_LANG_ALIASES[lower] ?? lower;
132
+ if (!BRAVE_SEARCH_LANG_CODES.has(canonical)) {
133
+ return undefined;
134
+ }
135
+ return canonical;
136
+ }
137
+
138
+ export function normalizeBraveCountry(value: string | undefined): string | undefined {
139
+ if (!value) {
140
+ return undefined;
141
+ }
142
+ const trimmed = value.trim();
143
+ if (!trimmed) {
144
+ return undefined;
145
+ }
146
+ const canonical = trimmed.toUpperCase();
147
+ return BRAVE_COUNTRY_CODES.has(canonical) ? canonical : "ALL";
148
+ }
149
+
150
+ function normalizeBraveUiLang(value: string | undefined): string | undefined {
151
+ if (!value) {
152
+ return undefined;
153
+ }
154
+ const trimmed = value.trim();
155
+ if (!trimmed) {
156
+ return undefined;
157
+ }
158
+ const match = trimmed.match(BRAVE_UI_LANG_LOCALE);
159
+ if (!match) {
160
+ return undefined;
161
+ }
162
+ const [, language, region] = match;
163
+ return `${normalizeLowercaseStringOrEmpty(language)}-${region.toUpperCase()}`;
164
+ }
165
+
166
+ export function resolveBraveConfig(searchConfig?: Record<string, unknown>): BraveConfig {
167
+ const brave = searchConfig?.brave;
168
+ return brave && typeof brave === "object" && !Array.isArray(brave) ? (brave as BraveConfig) : {};
169
+ }
170
+
171
+ export function resolveBraveMode(brave?: BraveConfig): "web" | "llm-context" {
172
+ return brave?.mode === "llm-context" ? "llm-context" : "web";
173
+ }
174
+
175
+ export function normalizeBraveLanguageParams(params: { search_lang?: string; ui_lang?: string }): {
176
+ search_lang?: string;
177
+ ui_lang?: string;
178
+ invalidField?: "search_lang" | "ui_lang";
179
+ } {
180
+ const rawSearchLang = normalizeOptionalString(params.search_lang);
181
+ const rawUiLang = normalizeOptionalString(params.ui_lang);
182
+ let searchLangCandidate = rawSearchLang;
183
+ let uiLangCandidate = rawUiLang;
184
+
185
+ if (normalizeBraveUiLang(rawSearchLang) && normalizeBraveSearchLang(rawUiLang)) {
186
+ searchLangCandidate = rawUiLang;
187
+ uiLangCandidate = rawSearchLang;
188
+ }
189
+
190
+ const search_lang = normalizeBraveSearchLang(searchLangCandidate);
191
+ if (searchLangCandidate && !search_lang) {
192
+ return { invalidField: "search_lang" };
193
+ }
194
+
195
+ const ui_lang = normalizeBraveUiLang(uiLangCandidate);
196
+ if (uiLangCandidate && !ui_lang) {
197
+ return { invalidField: "ui_lang" };
198
+ }
199
+
200
+ return { search_lang, ui_lang };
201
+ }
202
+
203
+ function resolveSiteName(url: string | undefined): string | undefined {
204
+ if (!url) {
205
+ return undefined;
206
+ }
207
+ try {
208
+ return new URL(url).hostname;
209
+ } catch {
210
+ return undefined;
211
+ }
212
+ }
213
+
214
+ export function mapBraveLlmContextResults(
215
+ data: BraveLlmContextResponse,
216
+ ): { url: string; title: string; snippets: string[]; siteName?: string }[] {
217
+ const genericResults = Array.isArray(data.grounding?.generic) ? data.grounding.generic : [];
218
+ return genericResults.map((entry) => ({
219
+ url: entry.url ?? "",
220
+ title: entry.title ?? "",
221
+ snippets: (entry.snippets ?? []).filter(
222
+ (snippet) => typeof snippet === "string" && snippet.length > 0,
223
+ ),
224
+ siteName: resolveSiteName(entry.url) || undefined,
225
+ }));
226
+ }