@kodelyth/brave-plugin 2026.5.42 → 2026.6.2
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/package.json +17 -2
- package/index.ts +0 -11
- package/src/brave-web-search-provider.runtime.ts +0 -570
- package/src/brave-web-search-provider.shared.ts +0 -227
- package/src/brave-web-search-provider.test.ts +0 -756
- package/src/brave-web-search-provider.ts +0 -186
- package/test-api.ts +0 -14
- package/tsconfig.json +0 -16
- package/web-search-contract-api.ts +0 -70
- package/web-search-provider.ts +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kodelyth/brave-plugin",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.6.2",
|
|
4
4
|
"description": "Klaw Brave plugin",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"devDependencies": {
|
|
11
|
-
"@kodelyth/plugin-sdk": "
|
|
11
|
+
"@kodelyth/plugin-sdk": "workspace:*"
|
|
12
12
|
},
|
|
13
13
|
"klaw": {
|
|
14
14
|
"extensions": [
|
|
@@ -29,6 +29,21 @@
|
|
|
29
29
|
"release": {
|
|
30
30
|
"publishToClawHub": true,
|
|
31
31
|
"publishToNpm": true
|
|
32
|
+
},
|
|
33
|
+
"runtimeExtensions": [
|
|
34
|
+
"./dist/index.js"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist/**",
|
|
39
|
+
"klaw.plugin.json"
|
|
40
|
+
],
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"klaw": ">=2026.5.39"
|
|
43
|
+
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"klaw": {
|
|
46
|
+
"optional": true
|
|
32
47
|
}
|
|
33
48
|
}
|
|
34
49
|
}
|
package/index.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { definePluginEntry } from "klaw/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
|
-
});
|
|
@@ -1,570 +0,0 @@
|
|
|
1
|
-
import { readProviderJsonResponse } from "klaw/plugin-sdk/provider-http";
|
|
2
|
-
import type { SearchConfigRecord } from "klaw/plugin-sdk/provider-web-search";
|
|
3
|
-
import {
|
|
4
|
-
buildSearchCacheKey,
|
|
5
|
-
DEFAULT_SEARCH_COUNT,
|
|
6
|
-
formatCliCommand,
|
|
7
|
-
normalizeFreshness,
|
|
8
|
-
parseIsoDateRange,
|
|
9
|
-
readCachedSearchPayload,
|
|
10
|
-
readConfiguredSecretString,
|
|
11
|
-
readNumberParam,
|
|
12
|
-
readProviderEnvValue,
|
|
13
|
-
readStringParam,
|
|
14
|
-
resolveSearchCacheTtlMs,
|
|
15
|
-
resolveSearchCount,
|
|
16
|
-
resolveSearchTimeoutSeconds,
|
|
17
|
-
resolveSiteName,
|
|
18
|
-
withSelfHostedWebSearchEndpoint,
|
|
19
|
-
withTrustedWebSearchEndpoint,
|
|
20
|
-
wrapWebContent,
|
|
21
|
-
writeCachedSearchPayload,
|
|
22
|
-
} from "klaw/plugin-sdk/provider-web-search";
|
|
23
|
-
import { createSubsystemLogger } from "klaw/plugin-sdk/runtime-env";
|
|
24
|
-
import {
|
|
25
|
-
assertHttpUrlTargetsPrivateNetwork,
|
|
26
|
-
isBlockedHostnameOrIp,
|
|
27
|
-
isPrivateIpAddress,
|
|
28
|
-
resolvePinnedHostnameWithPolicy,
|
|
29
|
-
} from "klaw/plugin-sdk/ssrf-runtime";
|
|
30
|
-
import {
|
|
31
|
-
type BraveLlmContextResponse,
|
|
32
|
-
mapBraveLlmContextResults,
|
|
33
|
-
normalizeBraveCountry,
|
|
34
|
-
normalizeBraveLanguageParams,
|
|
35
|
-
resolveBraveConfig,
|
|
36
|
-
resolveBraveMode,
|
|
37
|
-
} from "./brave-web-search-provider.shared.js";
|
|
38
|
-
|
|
39
|
-
const DEFAULT_BRAVE_BASE_URL = "https://api.search.brave.com";
|
|
40
|
-
const BRAVE_SEARCH_ENDPOINT_PATH = "/res/v1/web/search";
|
|
41
|
-
const BRAVE_LLM_CONTEXT_ENDPOINT_PATH = "/res/v1/llm/context";
|
|
42
|
-
const braveHttpLogger = createSubsystemLogger("brave/http");
|
|
43
|
-
type BraveEndpointMode = "selfHosted" | "strict";
|
|
44
|
-
|
|
45
|
-
type BraveSearchResult = {
|
|
46
|
-
title?: string;
|
|
47
|
-
url?: string;
|
|
48
|
-
description?: string;
|
|
49
|
-
age?: string;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
type BraveSearchResponse = {
|
|
53
|
-
web?: {
|
|
54
|
-
results?: BraveSearchResult[];
|
|
55
|
-
};
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
type BraveHttpDiagnostics = {
|
|
59
|
-
enabled?: boolean;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
function logBraveHttp(
|
|
63
|
-
diagnostics: BraveHttpDiagnostics | undefined,
|
|
64
|
-
event: string,
|
|
65
|
-
meta?: Record<string, unknown>,
|
|
66
|
-
): void {
|
|
67
|
-
if (!diagnostics?.enabled) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
braveHttpLogger.info(`brave http ${event}`, meta);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function describeBraveRequestUrl(url: URL): {
|
|
74
|
-
url: string;
|
|
75
|
-
query: string;
|
|
76
|
-
params: Record<string, string>;
|
|
77
|
-
} {
|
|
78
|
-
return {
|
|
79
|
-
url: url.toString(),
|
|
80
|
-
query: url.searchParams.get("q") ?? "",
|
|
81
|
-
params: Object.fromEntries(url.searchParams.entries()),
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function resolveBraveApiKey(searchConfig?: SearchConfigRecord): string | undefined {
|
|
86
|
-
return (
|
|
87
|
-
readConfiguredSecretString(searchConfig?.apiKey, "tools.web.search.apiKey") ??
|
|
88
|
-
readProviderEnvValue(["BRAVE_API_KEY"])
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function resolveBraveBaseUrl(braveConfig: { baseUrl?: unknown } | undefined): string {
|
|
93
|
-
const configured = readConfiguredSecretString(
|
|
94
|
-
braveConfig?.baseUrl,
|
|
95
|
-
"plugins.entries.brave.config.webSearch.baseUrl",
|
|
96
|
-
);
|
|
97
|
-
return configured?.replace(/\/+$/u, "") || DEFAULT_BRAVE_BASE_URL;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function buildBraveEndpointUrl(params: { baseUrl: string; endpointPath: string }): URL {
|
|
101
|
-
const url = new URL(params.baseUrl);
|
|
102
|
-
const basePath = url.pathname.replace(/\/+$/u, "");
|
|
103
|
-
url.pathname = `${basePath}${params.endpointPath}`;
|
|
104
|
-
url.search = "";
|
|
105
|
-
return url;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function braveEndpointTargetsPrivateNetwork(url: URL): Promise<boolean> {
|
|
109
|
-
if (isBlockedHostnameOrIp(url.hostname)) {
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
|
-
try {
|
|
113
|
-
const pinned = await resolvePinnedHostnameWithPolicy(url.hostname, {
|
|
114
|
-
policy: {
|
|
115
|
-
allowPrivateNetwork: true,
|
|
116
|
-
allowRfc2544BenchmarkRange: true,
|
|
117
|
-
},
|
|
118
|
-
});
|
|
119
|
-
return pinned.addresses.every((address) => isPrivateIpAddress(address));
|
|
120
|
-
} catch {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function validateBraveBaseUrl(baseUrl: string): Promise<BraveEndpointMode> {
|
|
126
|
-
let parsed: URL;
|
|
127
|
-
try {
|
|
128
|
-
parsed = new URL(baseUrl);
|
|
129
|
-
} catch {
|
|
130
|
-
throw new Error("Brave Search base URL must be a valid http:// or https:// URL.");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
134
|
-
throw new Error("Brave Search base URL must use http:// or https://.");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (parsed.protocol === "http:") {
|
|
138
|
-
await assertHttpUrlTargetsPrivateNetwork(parsed.toString(), {
|
|
139
|
-
dangerouslyAllowPrivateNetwork: true,
|
|
140
|
-
errorMessage:
|
|
141
|
-
"Brave Search HTTP base URL must target a trusted private or loopback host. Use https:// for public hosts.",
|
|
142
|
-
});
|
|
143
|
-
return "selfHosted";
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return (await braveEndpointTargetsPrivateNetwork(parsed)) ? "selfHosted" : "strict";
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function missingBraveKeyPayload() {
|
|
150
|
-
return {
|
|
151
|
-
error: "missing_brave_api_key",
|
|
152
|
-
message: `web_search (brave) needs a Brave Search API key. Run \`${formatCliCommand("klaw 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.`,
|
|
153
|
-
docs: "https://klaw.kodelyth.com/tools/web",
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function runBraveLlmContextSearch(params: {
|
|
158
|
-
baseUrl: string;
|
|
159
|
-
endpointMode: BraveEndpointMode;
|
|
160
|
-
query: string;
|
|
161
|
-
apiKey: string;
|
|
162
|
-
timeoutSeconds: number;
|
|
163
|
-
diagnostics?: BraveHttpDiagnostics;
|
|
164
|
-
country?: string;
|
|
165
|
-
search_lang?: string;
|
|
166
|
-
freshness?: string;
|
|
167
|
-
dateAfter?: string;
|
|
168
|
-
dateBefore?: string;
|
|
169
|
-
}): Promise<{
|
|
170
|
-
results: Array<{
|
|
171
|
-
url: string;
|
|
172
|
-
title: string;
|
|
173
|
-
snippets: string[];
|
|
174
|
-
siteName?: string;
|
|
175
|
-
}>;
|
|
176
|
-
sources?: BraveLlmContextResponse["sources"];
|
|
177
|
-
}> {
|
|
178
|
-
const url = buildBraveEndpointUrl({
|
|
179
|
-
baseUrl: params.baseUrl,
|
|
180
|
-
endpointPath: BRAVE_LLM_CONTEXT_ENDPOINT_PATH,
|
|
181
|
-
});
|
|
182
|
-
url.searchParams.set("q", params.query);
|
|
183
|
-
if (params.country) {
|
|
184
|
-
url.searchParams.set("country", params.country);
|
|
185
|
-
}
|
|
186
|
-
if (params.search_lang) {
|
|
187
|
-
url.searchParams.set("search_lang", params.search_lang);
|
|
188
|
-
}
|
|
189
|
-
if (params.freshness) {
|
|
190
|
-
url.searchParams.set("freshness", params.freshness);
|
|
191
|
-
} else if (params.dateAfter && params.dateBefore) {
|
|
192
|
-
url.searchParams.set("freshness", `${params.dateAfter}to${params.dateBefore}`);
|
|
193
|
-
} else if (params.dateAfter) {
|
|
194
|
-
url.searchParams.set(
|
|
195
|
-
"freshness",
|
|
196
|
-
`${params.dateAfter}to${new Date().toISOString().slice(0, 10)}`,
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
logBraveHttp(params.diagnostics, "request", {
|
|
201
|
-
mode: "llm-context",
|
|
202
|
-
...describeBraveRequestUrl(url),
|
|
203
|
-
});
|
|
204
|
-
const startedAt = Date.now();
|
|
205
|
-
const withEndpoint =
|
|
206
|
-
params.endpointMode === "selfHosted"
|
|
207
|
-
? withSelfHostedWebSearchEndpoint
|
|
208
|
-
: withTrustedWebSearchEndpoint;
|
|
209
|
-
return withEndpoint(
|
|
210
|
-
{
|
|
211
|
-
url: url.toString(),
|
|
212
|
-
timeoutSeconds: params.timeoutSeconds,
|
|
213
|
-
init: {
|
|
214
|
-
method: "GET",
|
|
215
|
-
headers: {
|
|
216
|
-
Accept: "application/json",
|
|
217
|
-
"X-Subscription-Token": params.apiKey,
|
|
218
|
-
},
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
async (response) => {
|
|
222
|
-
logBraveHttp(params.diagnostics, "response", {
|
|
223
|
-
mode: "llm-context",
|
|
224
|
-
status: response.status,
|
|
225
|
-
ok: response.ok,
|
|
226
|
-
durationMs: Date.now() - startedAt,
|
|
227
|
-
});
|
|
228
|
-
if (!response.ok) {
|
|
229
|
-
const detail = await response.text();
|
|
230
|
-
throw new Error(
|
|
231
|
-
`Brave LLM Context API error (${response.status}): ${detail || response.statusText}`,
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const data = await readProviderJsonResponse<BraveLlmContextResponse>(
|
|
236
|
-
response,
|
|
237
|
-
"Brave LLM Context API error",
|
|
238
|
-
);
|
|
239
|
-
return { results: mapBraveLlmContextResults(data), sources: data.sources };
|
|
240
|
-
},
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
async function runBraveWebSearch(params: {
|
|
245
|
-
baseUrl: string;
|
|
246
|
-
endpointMode: BraveEndpointMode;
|
|
247
|
-
query: string;
|
|
248
|
-
count: number;
|
|
249
|
-
apiKey: string;
|
|
250
|
-
timeoutSeconds: number;
|
|
251
|
-
diagnostics?: BraveHttpDiagnostics;
|
|
252
|
-
country?: string;
|
|
253
|
-
search_lang?: string;
|
|
254
|
-
ui_lang?: string;
|
|
255
|
-
freshness?: string;
|
|
256
|
-
dateAfter?: string;
|
|
257
|
-
dateBefore?: string;
|
|
258
|
-
}): Promise<Array<Record<string, unknown>>> {
|
|
259
|
-
const url = buildBraveEndpointUrl({
|
|
260
|
-
baseUrl: params.baseUrl,
|
|
261
|
-
endpointPath: BRAVE_SEARCH_ENDPOINT_PATH,
|
|
262
|
-
});
|
|
263
|
-
url.searchParams.set("q", params.query);
|
|
264
|
-
url.searchParams.set("count", String(params.count));
|
|
265
|
-
if (params.country) {
|
|
266
|
-
url.searchParams.set("country", params.country);
|
|
267
|
-
}
|
|
268
|
-
if (params.search_lang) {
|
|
269
|
-
url.searchParams.set("search_lang", params.search_lang);
|
|
270
|
-
}
|
|
271
|
-
if (params.ui_lang) {
|
|
272
|
-
url.searchParams.set("ui_lang", params.ui_lang);
|
|
273
|
-
}
|
|
274
|
-
if (params.freshness) {
|
|
275
|
-
url.searchParams.set("freshness", params.freshness);
|
|
276
|
-
} else if (params.dateAfter && params.dateBefore) {
|
|
277
|
-
url.searchParams.set("freshness", `${params.dateAfter}to${params.dateBefore}`);
|
|
278
|
-
} else if (params.dateAfter) {
|
|
279
|
-
url.searchParams.set(
|
|
280
|
-
"freshness",
|
|
281
|
-
`${params.dateAfter}to${new Date().toISOString().slice(0, 10)}`,
|
|
282
|
-
);
|
|
283
|
-
} else if (params.dateBefore) {
|
|
284
|
-
url.searchParams.set("freshness", `1970-01-01to${params.dateBefore}`);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
logBraveHttp(params.diagnostics, "request", {
|
|
288
|
-
mode: "web",
|
|
289
|
-
...describeBraveRequestUrl(url),
|
|
290
|
-
});
|
|
291
|
-
const startedAt = Date.now();
|
|
292
|
-
const withEndpoint =
|
|
293
|
-
params.endpointMode === "selfHosted"
|
|
294
|
-
? withSelfHostedWebSearchEndpoint
|
|
295
|
-
: withTrustedWebSearchEndpoint;
|
|
296
|
-
return withEndpoint(
|
|
297
|
-
{
|
|
298
|
-
url: url.toString(),
|
|
299
|
-
timeoutSeconds: params.timeoutSeconds,
|
|
300
|
-
init: {
|
|
301
|
-
method: "GET",
|
|
302
|
-
headers: {
|
|
303
|
-
Accept: "application/json",
|
|
304
|
-
"X-Subscription-Token": params.apiKey,
|
|
305
|
-
},
|
|
306
|
-
},
|
|
307
|
-
},
|
|
308
|
-
async (response) => {
|
|
309
|
-
logBraveHttp(params.diagnostics, "response", {
|
|
310
|
-
mode: "web",
|
|
311
|
-
status: response.status,
|
|
312
|
-
ok: response.ok,
|
|
313
|
-
durationMs: Date.now() - startedAt,
|
|
314
|
-
});
|
|
315
|
-
if (!response.ok) {
|
|
316
|
-
const detail = await response.text();
|
|
317
|
-
throw new Error(
|
|
318
|
-
`Brave Search API error (${response.status}): ${detail || response.statusText}`,
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const data = await readProviderJsonResponse<BraveSearchResponse>(
|
|
323
|
-
response,
|
|
324
|
-
"Brave Search API error",
|
|
325
|
-
);
|
|
326
|
-
const results = Array.isArray(data.web?.results) ? (data.web?.results ?? []) : [];
|
|
327
|
-
return results.map((entry) => {
|
|
328
|
-
const description = entry.description ?? "";
|
|
329
|
-
const title = entry.title ?? "";
|
|
330
|
-
const url = entry.url ?? "";
|
|
331
|
-
return {
|
|
332
|
-
title: title ? wrapWebContent(title, "web_search") : "",
|
|
333
|
-
url,
|
|
334
|
-
description: description ? wrapWebContent(description, "web_search") : "",
|
|
335
|
-
published: entry.age || undefined,
|
|
336
|
-
siteName: resolveSiteName(url) || undefined,
|
|
337
|
-
};
|
|
338
|
-
});
|
|
339
|
-
},
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
export async function executeBraveSearch(
|
|
344
|
-
args: Record<string, unknown>,
|
|
345
|
-
searchConfig?: SearchConfigRecord,
|
|
346
|
-
options?: {
|
|
347
|
-
diagnosticsEnabled?: boolean;
|
|
348
|
-
},
|
|
349
|
-
): Promise<Record<string, unknown>> {
|
|
350
|
-
const apiKey = resolveBraveApiKey(searchConfig);
|
|
351
|
-
if (!apiKey) {
|
|
352
|
-
return missingBraveKeyPayload();
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const braveConfig = resolveBraveConfig(searchConfig);
|
|
356
|
-
const braveMode = resolveBraveMode(braveConfig);
|
|
357
|
-
const braveBaseUrl = resolveBraveBaseUrl(braveConfig);
|
|
358
|
-
const braveEndpointMode = await validateBraveBaseUrl(braveBaseUrl);
|
|
359
|
-
const query = readStringParam(args, "query", { required: true });
|
|
360
|
-
const count =
|
|
361
|
-
readNumberParam(args, "count", { integer: true }) ?? searchConfig?.maxResults ?? undefined;
|
|
362
|
-
const country = normalizeBraveCountry(readStringParam(args, "country"));
|
|
363
|
-
const language = readStringParam(args, "language");
|
|
364
|
-
const search_lang = readStringParam(args, "search_lang");
|
|
365
|
-
const ui_lang = readStringParam(args, "ui_lang");
|
|
366
|
-
const normalizedLanguage = normalizeBraveLanguageParams({
|
|
367
|
-
search_lang: search_lang || language,
|
|
368
|
-
ui_lang,
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
if (normalizedLanguage.invalidField === "search_lang") {
|
|
372
|
-
return {
|
|
373
|
-
error: "invalid_search_lang",
|
|
374
|
-
message:
|
|
375
|
-
"search_lang must be a Brave-supported language code like 'en', 'en-gb', 'zh-hans', or 'zh-hant'.",
|
|
376
|
-
docs: "https://klaw.kodelyth.com/tools/web",
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
if (normalizedLanguage.invalidField === "ui_lang") {
|
|
380
|
-
return {
|
|
381
|
-
error: "invalid_ui_lang",
|
|
382
|
-
message: "ui_lang must be a language-region locale like 'en-US'.",
|
|
383
|
-
docs: "https://klaw.kodelyth.com/tools/web",
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
if (normalizedLanguage.ui_lang && braveMode === "llm-context") {
|
|
387
|
-
return {
|
|
388
|
-
error: "unsupported_ui_lang",
|
|
389
|
-
message:
|
|
390
|
-
"ui_lang is not supported by Brave llm-context mode. Remove ui_lang or use Brave web mode for locale-based UI hints.",
|
|
391
|
-
docs: "https://klaw.kodelyth.com/tools/web",
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const rawFreshness = readStringParam(args, "freshness");
|
|
396
|
-
const freshness = rawFreshness ? normalizeFreshness(rawFreshness, "brave") : undefined;
|
|
397
|
-
if (rawFreshness && !freshness) {
|
|
398
|
-
return {
|
|
399
|
-
error: "invalid_freshness",
|
|
400
|
-
message: "freshness must be day, week, month, or year.",
|
|
401
|
-
docs: "https://klaw.kodelyth.com/tools/web",
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const rawDateAfter = readStringParam(args, "date_after");
|
|
406
|
-
const rawDateBefore = readStringParam(args, "date_before");
|
|
407
|
-
if (rawFreshness && (rawDateAfter || rawDateBefore)) {
|
|
408
|
-
return {
|
|
409
|
-
error: "conflicting_time_filters",
|
|
410
|
-
message:
|
|
411
|
-
"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.",
|
|
412
|
-
docs: "https://klaw.kodelyth.com/tools/web",
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
const parsedDateRange = parseIsoDateRange({
|
|
416
|
-
rawDateAfter,
|
|
417
|
-
rawDateBefore,
|
|
418
|
-
invalidDateAfterMessage: "date_after must be YYYY-MM-DD format.",
|
|
419
|
-
invalidDateBeforeMessage: "date_before must be YYYY-MM-DD format.",
|
|
420
|
-
invalidDateRangeMessage: "date_after must be before date_before.",
|
|
421
|
-
});
|
|
422
|
-
if ("error" in parsedDateRange) {
|
|
423
|
-
return parsedDateRange;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const { dateAfter, dateBefore } = parsedDateRange;
|
|
427
|
-
if (braveMode === "llm-context") {
|
|
428
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
429
|
-
if (dateAfter && !dateBefore && dateAfter > today) {
|
|
430
|
-
return {
|
|
431
|
-
error: "invalid_date_range",
|
|
432
|
-
message: "date_after cannot be in the future for Brave llm-context mode.",
|
|
433
|
-
docs: "https://klaw.kodelyth.com/tools/web",
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
if (dateBefore && !dateAfter) {
|
|
437
|
-
return {
|
|
438
|
-
error: "unsupported_date_filter",
|
|
439
|
-
message:
|
|
440
|
-
"Brave llm-context mode requires date_after when date_before is set. Use a bounded date range or freshness.",
|
|
441
|
-
docs: "https://klaw.kodelyth.com/tools/web",
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
const llmContextDateEnd =
|
|
446
|
-
braveMode === "llm-context" && dateAfter
|
|
447
|
-
? (dateBefore ?? new Date().toISOString().slice(0, 10))
|
|
448
|
-
: dateBefore;
|
|
449
|
-
const cacheKey = buildSearchCacheKey(
|
|
450
|
-
braveMode === "llm-context"
|
|
451
|
-
? [
|
|
452
|
-
"brave",
|
|
453
|
-
braveMode,
|
|
454
|
-
braveBaseUrl,
|
|
455
|
-
query,
|
|
456
|
-
country,
|
|
457
|
-
normalizedLanguage.search_lang,
|
|
458
|
-
freshness,
|
|
459
|
-
dateAfter,
|
|
460
|
-
llmContextDateEnd,
|
|
461
|
-
]
|
|
462
|
-
: [
|
|
463
|
-
"brave",
|
|
464
|
-
braveMode,
|
|
465
|
-
braveBaseUrl,
|
|
466
|
-
query,
|
|
467
|
-
resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
|
|
468
|
-
country,
|
|
469
|
-
normalizedLanguage.search_lang,
|
|
470
|
-
normalizedLanguage.ui_lang,
|
|
471
|
-
freshness,
|
|
472
|
-
dateAfter,
|
|
473
|
-
dateBefore,
|
|
474
|
-
],
|
|
475
|
-
);
|
|
476
|
-
const diagnostics: BraveHttpDiagnostics = { enabled: options?.diagnosticsEnabled === true };
|
|
477
|
-
const cached = readCachedSearchPayload(cacheKey);
|
|
478
|
-
if (cached) {
|
|
479
|
-
logBraveHttp(diagnostics, "cache hit", { mode: braveMode, query, cacheKey });
|
|
480
|
-
return cached;
|
|
481
|
-
}
|
|
482
|
-
logBraveHttp(diagnostics, "cache miss", { mode: braveMode, query, cacheKey });
|
|
483
|
-
|
|
484
|
-
const start = Date.now();
|
|
485
|
-
const timeoutSeconds = resolveSearchTimeoutSeconds(searchConfig);
|
|
486
|
-
const cacheTtlMs = resolveSearchCacheTtlMs(searchConfig);
|
|
487
|
-
|
|
488
|
-
if (braveMode === "llm-context") {
|
|
489
|
-
const { results, sources } = await runBraveLlmContextSearch({
|
|
490
|
-
baseUrl: braveBaseUrl,
|
|
491
|
-
endpointMode: braveEndpointMode,
|
|
492
|
-
query,
|
|
493
|
-
apiKey,
|
|
494
|
-
timeoutSeconds,
|
|
495
|
-
diagnostics,
|
|
496
|
-
country: country ?? undefined,
|
|
497
|
-
search_lang: normalizedLanguage.search_lang,
|
|
498
|
-
freshness,
|
|
499
|
-
dateAfter,
|
|
500
|
-
dateBefore,
|
|
501
|
-
});
|
|
502
|
-
const payload = {
|
|
503
|
-
query,
|
|
504
|
-
provider: "brave",
|
|
505
|
-
mode: "llm-context" as const,
|
|
506
|
-
count: results.length,
|
|
507
|
-
tookMs: Date.now() - start,
|
|
508
|
-
externalContent: {
|
|
509
|
-
untrusted: true,
|
|
510
|
-
source: "web_search",
|
|
511
|
-
provider: "brave",
|
|
512
|
-
wrapped: true,
|
|
513
|
-
},
|
|
514
|
-
results: results.map((entry) => ({
|
|
515
|
-
title: entry.title ? wrapWebContent(entry.title, "web_search") : "",
|
|
516
|
-
url: entry.url,
|
|
517
|
-
snippets: entry.snippets.map((snippet) => wrapWebContent(snippet, "web_search")),
|
|
518
|
-
siteName: entry.siteName,
|
|
519
|
-
})),
|
|
520
|
-
sources,
|
|
521
|
-
};
|
|
522
|
-
writeCachedSearchPayload(cacheKey, payload, cacheTtlMs);
|
|
523
|
-
logBraveHttp(diagnostics, "cache write", {
|
|
524
|
-
mode: "llm-context",
|
|
525
|
-
query,
|
|
526
|
-
cacheKey,
|
|
527
|
-
ttlMs: cacheTtlMs,
|
|
528
|
-
count: results.length,
|
|
529
|
-
});
|
|
530
|
-
return payload;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
const results = await runBraveWebSearch({
|
|
534
|
-
baseUrl: braveBaseUrl,
|
|
535
|
-
endpointMode: braveEndpointMode,
|
|
536
|
-
query,
|
|
537
|
-
count: resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
|
|
538
|
-
apiKey,
|
|
539
|
-
timeoutSeconds,
|
|
540
|
-
diagnostics,
|
|
541
|
-
country: country ?? undefined,
|
|
542
|
-
search_lang: normalizedLanguage.search_lang,
|
|
543
|
-
ui_lang: normalizedLanguage.ui_lang,
|
|
544
|
-
freshness,
|
|
545
|
-
dateAfter,
|
|
546
|
-
dateBefore,
|
|
547
|
-
});
|
|
548
|
-
const payload = {
|
|
549
|
-
query,
|
|
550
|
-
provider: "brave",
|
|
551
|
-
count: results.length,
|
|
552
|
-
tookMs: Date.now() - start,
|
|
553
|
-
externalContent: {
|
|
554
|
-
untrusted: true,
|
|
555
|
-
source: "web_search",
|
|
556
|
-
provider: "brave",
|
|
557
|
-
wrapped: true,
|
|
558
|
-
},
|
|
559
|
-
results,
|
|
560
|
-
};
|
|
561
|
-
writeCachedSearchPayload(cacheKey, payload, cacheTtlMs);
|
|
562
|
-
logBraveHttp(diagnostics, "cache write", {
|
|
563
|
-
mode: "web",
|
|
564
|
-
query,
|
|
565
|
-
cacheKey,
|
|
566
|
-
ttlMs: cacheTtlMs,
|
|
567
|
-
count: results.length,
|
|
568
|
-
});
|
|
569
|
-
return payload;
|
|
570
|
-
}
|