@zeldrisho/pi-web-search 0.5.0 → 0.6.0
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/CHANGELOG.md +9 -1
- package/README.md +5 -3
- package/package.json +8 -8
- package/src/brave.ts +47 -5
- package/src/credentials.ts +72 -0
- package/src/index.ts +23 -2
- package/src/limits.ts +3 -1
- package/src/provider.ts +0 -7
- package/src/search.ts +36 -4
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2026-08-24
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Resolve `BRAVE_SEARCH_API_KEY` from the process environment, the workspace `.env`, or the agent-global `.env`, and report only the credential source in `details.apiKeySource`
|
|
15
|
+
- Add web-mode search options: `country` code filter, `safesearch` level (`off`/`moderate`/`strict`, default `moderate`), and `extraSnippets` to append Brave's additional excerpts per result; using them in context mode now fails with a clear error
|
|
16
|
+
|
|
10
17
|
## [0.5.0] - 2026-08-20
|
|
11
18
|
|
|
12
19
|
### Added
|
|
@@ -60,7 +67,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
60
67
|
|
|
61
68
|
- Add bounded Brave web search ([cbc9491](https://github.com/zeldrisho/pi-packages/commit/cbc9491dce36555ab91b6bef203ec8b380596a89))
|
|
62
69
|
|
|
63
|
-
[Unreleased]: https://github.com/zeldrisho/pi-packages/compare/pi-web-search-v0.
|
|
70
|
+
[Unreleased]: https://github.com/zeldrisho/pi-packages/compare/pi-web-search-v0.6.0...HEAD
|
|
71
|
+
[0.6.0]: https://github.com/zeldrisho/pi-packages/compare/pi-web-search-v0.5.0...pi-web-search-v0.6.0
|
|
64
72
|
[0.5.0]: https://github.com/zeldrisho/pi-packages/compare/pi-web-search-v0.4.2...pi-web-search-v0.5.0
|
|
65
73
|
[0.4.2]: https://github.com/zeldrisho/pi-packages/compare/pi-web-search-v0.4.1...pi-web-search-v0.4.2
|
|
66
74
|
[0.4.1]: https://github.com/zeldrisho/pi-packages/compare/pi-web-search-v0.4.0...pi-web-search-v0.4.1
|
package/README.md
CHANGED
|
@@ -23,15 +23,17 @@ export BRAVE_SEARCH_API_KEY="your-api-key"
|
|
|
23
23
|
pi
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
If
|
|
26
|
+
If the environment variable is not set, the tool also resolves the key from a `BRAVE_SEARCH_API_KEY=` line in the workspace `.env`, and then in the agent-global `.env` (in that order). Only where the key was found is ever reported; the value itself never appears in output or errors.
|
|
27
|
+
|
|
28
|
+
If Pi is already running when you set the key, run `/reload` in that Pi session.
|
|
27
29
|
|
|
28
30
|
## Usage
|
|
29
31
|
|
|
30
|
-
The `web_search` tool returns compact web results by default, making it suitable for discovering current sources and URLs. Set `mode` to `context` to call only Brave's LLM Context API and return provider-extracted snippets. Context queries are limited to 400 characters; web-mode queries are limited to
|
|
32
|
+
The `web_search` tool returns compact web results by default, making it suitable for discovering current sources and URLs. Set `mode` to `context` to call only Brave's LLM Context API and return provider-extracted snippets. Context queries are limited to 400 characters; web-mode queries are limited to 400 characters. Because the provider rejects union schemas, the tool schema accepts up to the web-mode query length for either mode; the tighter context-mode limit is enforced at runtime when the request executes, not in the schema.
|
|
31
33
|
|
|
32
34
|
Context snippets may not reflect the current live page. The tool does not fetch result URLs. When live-page inspection is needed, call `web_fetch` explicitly on the relevant URL.
|
|
33
35
|
|
|
34
|
-
Searches accept an optional result count of up to 20, a freshness filter (`day`, `week`, `month`, or `year`), and a language code such as `en` or `en-US`.
|
|
36
|
+
Searches accept an optional result count of up to 20, a freshness filter (`day`, `week`, `month`, or `year`), and a language code such as `en` or `en-US`. Web mode additionally accepts a country code (`US`, `DE`, …), a SafeSearch level (`off`, `moderate`, or `strict`; default `moderate`), and an `extraSnippets` flag that appends Brave's additional excerpt paragraphs to each result snippet. These three options apply only to web mode; requests that use them with `mode: "context"` fail with a clear error.
|
|
35
37
|
|
|
36
38
|
Identical searches are cached in byte-bounded memory for a limited time. Concurrent identical searches share one provider request; cancelling one caller does not cancel work still needed by another. In Pi's interactive UI, results use Pi's standard collapsed preview; use the configured tool-expansion shortcut (`Ctrl+O` by default) to show all visible tool output. Output sent to the agent remains bounded, and when a result is truncated, the complete output is written to a temporary file that is removed when the Pi session shuts down.
|
|
37
39
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeldrisho/pi-web-search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Pi extension for bounded Brave web search and LLM context retrieval",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"brave-search",
|
|
@@ -32,13 +32,13 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@earendil-works/pi-ai": "^0.84.
|
|
36
|
-
"@earendil-works/pi-coding-agent": "^0.84.
|
|
37
|
-
"@earendil-works/pi-tui": "^0.84.
|
|
38
|
-
"typebox": "^1.
|
|
39
|
-
"typescript": "^7.0.
|
|
40
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@0.
|
|
41
|
-
"vite-plus": "0.
|
|
35
|
+
"@earendil-works/pi-ai": "^0.84.2",
|
|
36
|
+
"@earendil-works/pi-coding-agent": "^0.84.2",
|
|
37
|
+
"@earendil-works/pi-tui": "^0.84.2",
|
|
38
|
+
"typebox": "^1.3.16",
|
|
39
|
+
"typescript": "^7.0.2",
|
|
40
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.3.0",
|
|
41
|
+
"vite-plus": "0.3.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@earendil-works/pi-ai": "*",
|
package/src/brave.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { normalizeText, requestJson } from "./provider";
|
|
|
8
8
|
|
|
9
9
|
export type Provider = "brave";
|
|
10
10
|
export type Freshness = "day" | "week" | "month" | "year";
|
|
11
|
+
export type SafesearchMode = "off" | "moderate" | "strict";
|
|
11
12
|
export type SearchMode = "web" | "context";
|
|
12
13
|
|
|
13
14
|
export type ResultQuality = "high" | "medium" | "low";
|
|
@@ -24,6 +25,7 @@ interface BraveWebResult {
|
|
|
24
25
|
title?: string;
|
|
25
26
|
url?: string;
|
|
26
27
|
description?: string;
|
|
28
|
+
extra_snippets?: string[];
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
interface BraveWebResponse {
|
|
@@ -45,6 +47,13 @@ interface BraveContextResponse {
|
|
|
45
47
|
grounding?: { generic?: BraveContextResult[] };
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
/** Web-mode-only search options that the context endpoint does not support. */
|
|
51
|
+
export interface WebSearchExtras {
|
|
52
|
+
country?: string;
|
|
53
|
+
safesearch?: SafesearchMode;
|
|
54
|
+
extraSnippets?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
48
57
|
/**
|
|
49
58
|
* Classifies a search result by how much usable, sourced information it carries.
|
|
50
59
|
*
|
|
@@ -62,6 +71,7 @@ const BRAVE_MAX_CONTEXT_TOKENS = 2_048;
|
|
|
62
71
|
const BRAVE_MAX_SNIPPETS = 15;
|
|
63
72
|
const BRAVE_MAX_TOKENS_PER_URL = 1_024;
|
|
64
73
|
const BRAVE_MAX_SNIPPETS_PER_URL = 3;
|
|
74
|
+
const BRAVE_MAX_EXTRA_SNIPPETS = 5;
|
|
65
75
|
|
|
66
76
|
/**
|
|
67
77
|
* Validates a search query and requested result count against the provider limits.
|
|
@@ -71,7 +81,22 @@ const BRAVE_MAX_SNIPPETS_PER_URL = 3;
|
|
|
71
81
|
* @param mode - The search mode whose query limit applies
|
|
72
82
|
* @throws If the query exceeds the mode limit or the result count is outside the allowed range
|
|
73
83
|
*/
|
|
74
|
-
export function validateProviderRequest(
|
|
84
|
+
export function validateProviderRequest(
|
|
85
|
+
query: string,
|
|
86
|
+
count: number,
|
|
87
|
+
mode: SearchMode,
|
|
88
|
+
extras?: WebSearchExtras,
|
|
89
|
+
): void {
|
|
90
|
+
if (
|
|
91
|
+
mode === "context" &&
|
|
92
|
+
(extras?.country !== undefined ||
|
|
93
|
+
extras?.safesearch !== undefined ||
|
|
94
|
+
extras?.extraSnippets !== undefined)
|
|
95
|
+
) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
"The country, safesearch, and extraSnippets options are only supported in web mode.",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
75
100
|
const maximumQueryCharacters =
|
|
76
101
|
mode === "context" ? SEARCH_CONTEXT_MAX_QUERY_CHARACTERS : SEARCH_WEB_MAX_QUERY_CHARACTERS;
|
|
77
102
|
if (query.length > maximumQueryCharacters) {
|
|
@@ -151,6 +176,8 @@ function braveSnippetToMarkdown(value: string): string {
|
|
|
151
176
|
*
|
|
152
177
|
* @param query - The search query
|
|
153
178
|
* @param count - The requested number of results
|
|
179
|
+
* @param apiKey - The resolved Brave subscription token (never logged or echoed)
|
|
180
|
+
* @param extras - Optional web-mode filters: country, safesearch level, extra snippets
|
|
154
181
|
* @returns Normalized web search results
|
|
155
182
|
*/
|
|
156
183
|
export async function searchBraveWeb(
|
|
@@ -159,14 +186,18 @@ export async function searchBraveWeb(
|
|
|
159
186
|
freshness: Freshness | undefined,
|
|
160
187
|
language: string | undefined,
|
|
161
188
|
signal: AbortSignal | undefined,
|
|
189
|
+
apiKey: string,
|
|
190
|
+
extras: WebSearchExtras = {},
|
|
162
191
|
): Promise<SearchResult[]> {
|
|
163
192
|
validateProviderRequest(query, count, "web");
|
|
164
193
|
const url = new URL("https://api.search.brave.com/res/v1/web/search");
|
|
165
194
|
url.searchParams.set("q", query);
|
|
166
195
|
url.searchParams.set("count", String(count));
|
|
167
|
-
url.searchParams.set("safesearch", "moderate");
|
|
196
|
+
url.searchParams.set("safesearch", extras.safesearch ?? "moderate");
|
|
168
197
|
url.searchParams.set("text_decorations", "false");
|
|
169
198
|
if (language) url.searchParams.set("search_lang", language);
|
|
199
|
+
if (extras.country) url.searchParams.set("country", extras.country);
|
|
200
|
+
if (extras.extraSnippets) url.searchParams.set("extra_snippets", "true");
|
|
170
201
|
if (freshness)
|
|
171
202
|
url.searchParams.set(
|
|
172
203
|
"freshness",
|
|
@@ -178,7 +209,7 @@ export async function searchBraveWeb(
|
|
|
178
209
|
{
|
|
179
210
|
headers: {
|
|
180
211
|
Accept: "application/json",
|
|
181
|
-
"X-Subscription-Token":
|
|
212
|
+
"X-Subscription-Token": apiKey,
|
|
182
213
|
},
|
|
183
214
|
},
|
|
184
215
|
signal,
|
|
@@ -186,7 +217,16 @@ export async function searchBraveWeb(
|
|
|
186
217
|
|
|
187
218
|
return (data.web?.results ?? []).map((item) => {
|
|
188
219
|
const title = normalizeText(item.title ?? "", 300);
|
|
189
|
-
|
|
220
|
+
let snippet = normalizeText(item.description ?? "", 600);
|
|
221
|
+
if (extras.extraSnippets && item.extra_snippets?.length) {
|
|
222
|
+
const additional = item.extra_snippets
|
|
223
|
+
.slice(0, BRAVE_MAX_EXTRA_SNIPPETS)
|
|
224
|
+
.map((value) => normalizeText(value ?? "", 300))
|
|
225
|
+
.filter(Boolean);
|
|
226
|
+
if (additional.length) {
|
|
227
|
+
snippet = normalizeText([snippet, ...additional].filter(Boolean).join("\n\n"), 2000);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
190
230
|
return {
|
|
191
231
|
title,
|
|
192
232
|
url: normalizeUrl(item.url ?? ""),
|
|
@@ -199,6 +239,7 @@ export async function searchBraveWeb(
|
|
|
199
239
|
/**
|
|
200
240
|
* Searches Brave's context API and maps grounding results to normalized search results.
|
|
201
241
|
*
|
|
242
|
+
* @param apiKey - The resolved Brave subscription token (never logged or echoed)
|
|
202
243
|
* @returns Search results containing normalized titles and URLs with deduplicated Markdown snippets.
|
|
203
244
|
*/
|
|
204
245
|
export async function searchBraveContext(
|
|
@@ -207,6 +248,7 @@ export async function searchBraveContext(
|
|
|
207
248
|
freshness: Freshness | undefined,
|
|
208
249
|
language: string | undefined,
|
|
209
250
|
signal: AbortSignal | undefined,
|
|
251
|
+
apiKey: string,
|
|
210
252
|
): Promise<SearchResult[]> {
|
|
211
253
|
validateProviderRequest(query, count, "context");
|
|
212
254
|
|
|
@@ -231,7 +273,7 @@ export async function searchBraveContext(
|
|
|
231
273
|
{
|
|
232
274
|
headers: {
|
|
233
275
|
Accept: "application/json",
|
|
234
|
-
"X-Subscription-Token":
|
|
276
|
+
"X-Subscription-Token": apiKey,
|
|
235
277
|
},
|
|
236
278
|
},
|
|
237
279
|
signal,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { open } from "node:fs/promises";
|
|
2
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export type ApiKeySource = "environment" | "workspace .env" | "agent .env";
|
|
6
|
+
|
|
7
|
+
export interface ResolvedApiKey {
|
|
8
|
+
key: string;
|
|
9
|
+
/** Where the key was found; only the source is ever reported, never the value. */
|
|
10
|
+
source: ApiKeySource;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const KEY_NAME = "BRAVE_SEARCH_API_KEY";
|
|
14
|
+
const MAX_DOT_ENV_BYTES = 64 * 1024;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extracts the Brave API key from untrusted `.env` text.
|
|
18
|
+
*
|
|
19
|
+
* Only the extracted value is used; file contents are never echoed into
|
|
20
|
+
* errors or tool output.
|
|
21
|
+
*/
|
|
22
|
+
export function extractKeyValue(text: string): string | undefined {
|
|
23
|
+
for (const line of text.split(/\r?\n/)) {
|
|
24
|
+
const match = line.match(/^\s*BRAVE_SEARCH_API_KEY\s*=\s*(.*)$/);
|
|
25
|
+
if (!match) continue;
|
|
26
|
+
let value = match[1].trim();
|
|
27
|
+
if (
|
|
28
|
+
value.length >= 2 &&
|
|
29
|
+
((value.startsWith('"') && value.endsWith('"')) ||
|
|
30
|
+
(value.startsWith("'") && value.endsWith("'")))
|
|
31
|
+
) {
|
|
32
|
+
value = value.slice(1, -1);
|
|
33
|
+
}
|
|
34
|
+
return value || undefined;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function readDotEnvKey(path: string): Promise<string | undefined> {
|
|
40
|
+
let handle: Awaited<ReturnType<typeof open>> | undefined;
|
|
41
|
+
try {
|
|
42
|
+
handle = await open(path, "r");
|
|
43
|
+
const buffer = Buffer.alloc(MAX_DOT_ENV_BYTES);
|
|
44
|
+
const { bytesRead } = await handle.read(buffer, 0, MAX_DOT_ENV_BYTES, 0);
|
|
45
|
+
return extractKeyValue(buffer.toString("utf8", 0, bytesRead));
|
|
46
|
+
} catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
} finally {
|
|
49
|
+
await handle?.close().catch(() => undefined);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolves the Brave API key without exposing its value.
|
|
55
|
+
*
|
|
56
|
+
* Resolution order: process environment, workspace `.env`, agent-global `.env`.
|
|
57
|
+
*
|
|
58
|
+
* @param cwd - The workspace directory whose `.env` is consulted second
|
|
59
|
+
* @returns The key and its source, or `undefined` when no source provides one
|
|
60
|
+
*/
|
|
61
|
+
export async function resolveApiKey(cwd: string): Promise<ResolvedApiKey | undefined> {
|
|
62
|
+
const fromEnvironment = process.env[KEY_NAME];
|
|
63
|
+
if (fromEnvironment) return { key: fromEnvironment, source: "environment" };
|
|
64
|
+
|
|
65
|
+
const fromWorkspace = await readDotEnvKey(join(cwd, ".env"));
|
|
66
|
+
if (fromWorkspace) return { key: fromWorkspace, source: "workspace .env" };
|
|
67
|
+
|
|
68
|
+
const fromAgent = await readDotEnvKey(join(getAgentDir(), ".env"));
|
|
69
|
+
if (fromAgent) return { key: fromAgent, source: "agent .env" };
|
|
70
|
+
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,8 +5,10 @@ import { Type } from "typebox";
|
|
|
5
5
|
import {
|
|
6
6
|
SEARCH_CONTEXT_MAX_QUERY_CHARACTERS,
|
|
7
7
|
SEARCH_DEFAULT_RESULT_COUNT,
|
|
8
|
+
SEARCH_MAX_COUNTRY_CHARACTERS,
|
|
8
9
|
SEARCH_MAX_LANGUAGE_CHARACTERS,
|
|
9
10
|
SEARCH_MAX_RESULT_COUNT,
|
|
11
|
+
SEARCH_MIN_COUNTRY_CHARACTERS,
|
|
10
12
|
SEARCH_MIN_LANGUAGE_CHARACTERS,
|
|
11
13
|
SEARCH_MIN_RESULT_COUNT,
|
|
12
14
|
SEARCH_WEB_MAX_QUERY_CHARACTERS,
|
|
@@ -54,6 +56,25 @@ export const webSearchParameters = Type.Object({
|
|
|
54
56
|
description: "Optional language code, such as en or en-US",
|
|
55
57
|
}),
|
|
56
58
|
),
|
|
59
|
+
country: Type.Optional(
|
|
60
|
+
Type.String({
|
|
61
|
+
minLength: SEARCH_MIN_COUNTRY_CHARACTERS,
|
|
62
|
+
maxLength: SEARCH_MAX_COUNTRY_CHARACTERS,
|
|
63
|
+
description:
|
|
64
|
+
"Optional country code to boost or restrict results, such as US, DE, or CH (web mode only)",
|
|
65
|
+
}),
|
|
66
|
+
),
|
|
67
|
+
safesearch: Type.Optional(
|
|
68
|
+
StringEnum(["off", "moderate", "strict"] as const, {
|
|
69
|
+
description: "Optional SafeSearch level (web mode only; default: moderate)",
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
72
|
+
extraSnippets: Type.Optional(
|
|
73
|
+
Type.Boolean({
|
|
74
|
+
description:
|
|
75
|
+
"Request additional excerpt paragraphs per web result when Brave has them, appended to each snippet (web mode only)",
|
|
76
|
+
}),
|
|
77
|
+
),
|
|
57
78
|
});
|
|
58
79
|
|
|
59
80
|
export default function (pi: ExtensionAPI) {
|
|
@@ -81,8 +102,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
81
102
|
);
|
|
82
103
|
},
|
|
83
104
|
|
|
84
|
-
async execute(_toolCallId, params, signal, onUpdate) {
|
|
85
|
-
return runtime.execute(params, signal, onUpdate);
|
|
105
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
106
|
+
return runtime.execute(params, signal, onUpdate, ctx?.cwd);
|
|
86
107
|
},
|
|
87
108
|
|
|
88
109
|
renderResult(result, { expanded, isPartial }, theme) {
|
package/src/limits.ts
CHANGED
|
@@ -2,6 +2,8 @@ export const SEARCH_DEFAULT_RESULT_COUNT = 5;
|
|
|
2
2
|
export const SEARCH_MIN_RESULT_COUNT = 1;
|
|
3
3
|
export const SEARCH_MAX_RESULT_COUNT = 20;
|
|
4
4
|
export const SEARCH_CONTEXT_MAX_QUERY_CHARACTERS = 400;
|
|
5
|
-
export const SEARCH_WEB_MAX_QUERY_CHARACTERS =
|
|
5
|
+
export const SEARCH_WEB_MAX_QUERY_CHARACTERS = 400;
|
|
6
6
|
export const SEARCH_MIN_LANGUAGE_CHARACTERS = 2;
|
|
7
7
|
export const SEARCH_MAX_LANGUAGE_CHARACTERS = 20;
|
|
8
|
+
export const SEARCH_MIN_COUNTRY_CHARACTERS = 2;
|
|
9
|
+
export const SEARCH_MAX_COUNTRY_CHARACTERS = 2;
|
package/src/provider.ts
CHANGED
|
@@ -2,13 +2,6 @@ const REQUEST_TIMEOUT_MS = 20_000;
|
|
|
2
2
|
const SEARCH_MAX_RESPONSE_BYTES = 2_000_000;
|
|
3
3
|
const SEARCH_ERROR_EXCERPT_BYTES = 8_192;
|
|
4
4
|
|
|
5
|
-
export function configuredProvider(): "brave" {
|
|
6
|
-
if (!process.env.BRAVE_SEARCH_API_KEY) {
|
|
7
|
-
throw new Error("BRAVE_SEARCH_API_KEY is required for web search. Set it, then run /reload.");
|
|
8
|
-
}
|
|
9
|
-
return "brave";
|
|
10
|
-
}
|
|
11
|
-
|
|
12
5
|
export function normalizeText(value: string, maxLength: number): string {
|
|
13
6
|
const text = value
|
|
14
7
|
.replace(/<[^>]*>/g, " ")
|
package/src/search.ts
CHANGED
|
@@ -14,14 +14,15 @@ import {
|
|
|
14
14
|
validateProviderRequest,
|
|
15
15
|
type Freshness,
|
|
16
16
|
type Provider,
|
|
17
|
+
type SafesearchMode,
|
|
17
18
|
type SearchMode,
|
|
18
19
|
type SearchResult,
|
|
19
20
|
} from "./brave";
|
|
20
21
|
import { ExpiringLruCache, stableKeyHash, type CachePersistence } from "./cache";
|
|
22
|
+
import { resolveApiKey, type ApiKeySource } from "./credentials";
|
|
21
23
|
import { formatResults } from "./format-results";
|
|
22
24
|
import { InflightCoalescer } from "./inflight";
|
|
23
25
|
import { SEARCH_DEFAULT_RESULT_COUNT } from "./limits";
|
|
24
|
-
import { configuredProvider } from "./provider";
|
|
25
26
|
|
|
26
27
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1_000;
|
|
27
28
|
const CACHE_MAX_ENTRIES = 100;
|
|
@@ -52,6 +53,9 @@ export interface SearchParameters {
|
|
|
52
53
|
freshness?: Freshness;
|
|
53
54
|
mode?: SearchMode;
|
|
54
55
|
language?: string;
|
|
56
|
+
country?: string;
|
|
57
|
+
safesearch?: SafesearchMode;
|
|
58
|
+
extraSnippets?: boolean;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
export interface SearchTruncationDetails {
|
|
@@ -72,6 +76,8 @@ export interface SearchDetails {
|
|
|
72
76
|
results: SearchResult[];
|
|
73
77
|
evidence: SearchEvidence;
|
|
74
78
|
cached: boolean;
|
|
79
|
+
/** Where the Brave API key was found; the key value itself is never reported. */
|
|
80
|
+
apiKeySource: ApiKeySource;
|
|
75
81
|
truncated: boolean;
|
|
76
82
|
fullOutputPath?: string;
|
|
77
83
|
truncation: SearchTruncationDetails;
|
|
@@ -114,14 +120,26 @@ export class SearchRuntime {
|
|
|
114
120
|
params: SearchParameters,
|
|
115
121
|
signal: AbortSignal | undefined,
|
|
116
122
|
onUpdate: ((update: SearchUpdate) => void) | undefined,
|
|
123
|
+
cwd: string = process.cwd(),
|
|
117
124
|
) {
|
|
118
125
|
const query = params.query.trim();
|
|
119
126
|
if (!query) throw new Error("Search query cannot be empty.");
|
|
120
127
|
|
|
121
128
|
const count = params.count ?? SEARCH_DEFAULT_RESULT_COUNT;
|
|
122
129
|
const mode = params.mode ?? "web";
|
|
123
|
-
|
|
124
|
-
|
|
130
|
+
const extras = {
|
|
131
|
+
country: params.country,
|
|
132
|
+
safesearch: params.safesearch,
|
|
133
|
+
extraSnippets: params.extraSnippets,
|
|
134
|
+
};
|
|
135
|
+
validateProviderRequest(query, count, mode, extras);
|
|
136
|
+
const credentials = await resolveApiKey(cwd);
|
|
137
|
+
if (!credentials) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
"BRAVE_SEARCH_API_KEY is required for web search. Set it in the environment, the workspace .env, or the agent .env, then run /reload.",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const provider: Provider = "brave";
|
|
125
143
|
const cacheKey = JSON.stringify({
|
|
126
144
|
provider,
|
|
127
145
|
mode,
|
|
@@ -129,6 +147,10 @@ export class SearchRuntime {
|
|
|
129
147
|
count,
|
|
130
148
|
freshness: params.freshness,
|
|
131
149
|
language: params.language,
|
|
150
|
+
country: params.country,
|
|
151
|
+
safesearch: params.safesearch,
|
|
152
|
+
extraSnippets: params.extraSnippets,
|
|
153
|
+
cwd,
|
|
132
154
|
});
|
|
133
155
|
const cachedEntry = searchCache.get(cacheKey);
|
|
134
156
|
const cached = cachedEntry !== undefined;
|
|
@@ -157,8 +179,17 @@ export class SearchRuntime {
|
|
|
157
179
|
params.freshness,
|
|
158
180
|
params.language,
|
|
159
181
|
sharedSignal,
|
|
182
|
+
credentials.key,
|
|
160
183
|
)
|
|
161
|
-
: await searchBraveWeb(
|
|
184
|
+
: await searchBraveWeb(
|
|
185
|
+
query,
|
|
186
|
+
count,
|
|
187
|
+
params.freshness,
|
|
188
|
+
params.language,
|
|
189
|
+
sharedSignal,
|
|
190
|
+
credentials.key,
|
|
191
|
+
extras,
|
|
192
|
+
);
|
|
162
193
|
const bounded = found.filter((result) => result.url).slice(0, count);
|
|
163
194
|
searchCache.set(cacheKey, bounded, Date.now() + CACHE_TTL_MS);
|
|
164
195
|
return bounded;
|
|
@@ -209,6 +240,7 @@ export class SearchRuntime {
|
|
|
209
240
|
results,
|
|
210
241
|
evidence,
|
|
211
242
|
cached,
|
|
243
|
+
apiKeySource: credentials.source,
|
|
212
244
|
truncated: truncation.truncated,
|
|
213
245
|
fullOutputPath,
|
|
214
246
|
truncation: {
|