@zeldrisho/pi-web-search 0.10.0 → 0.12.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 +27 -1
- package/LICENSE +1 -1
- package/README.md +9 -18
- package/package.json +8 -8
- package/src/answers.ts +147 -0
- package/src/brave.ts +30 -293
- package/src/credentials.ts +15 -9
- package/src/index.ts +5 -13
- package/src/limits.ts +2 -13
- package/src/provider.ts +17 -6
- package/src/request-validation.ts +9 -13
- package/src/search.ts +147 -114
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.12.0] - 2026-10-20
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Simplify the model-facing web search tool description.
|
|
15
|
+
- Remove the model-facing mode selector and automatically try a Brave-generated answer when web search returns no usable results.
|
|
16
|
+
- Remove provider branding from progress notices.
|
|
17
|
+
|
|
18
|
+
## [0.11.0] - 2026-10-19
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Add explicit `answers` mode for Brave-generated answers with parsed source citations and usage metadata, using a separate `BRAVE_ANSWERS_API_KEY` credential
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Simplify the user guide and clarify the query limit, persistent caching, and Answers mode behavior.
|
|
27
|
+
- Show recovery guidance only for empty or partial web results and remove the provider/mode line from response text
|
|
28
|
+
- Group duplicate Answers source URLs in rendered output while preserving inline citation numbers and original metadata
|
|
29
|
+
|
|
30
|
+
### Removed
|
|
31
|
+
|
|
32
|
+
- Remove context mode, its tuning options, and the spellcheck and freshness request parameters; modes remain separate with no automatic switching
|
|
33
|
+
|
|
10
34
|
## [0.10.0] - 2026-10-12
|
|
11
35
|
|
|
12
36
|
### Added
|
|
@@ -122,7 +146,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
122
146
|
|
|
123
147
|
- Add bounded Brave web search ([cbc9491](https://github.com/zeldrisho/pi-packages/commit/cbc9491dce36555ab91b6bef203ec8b380596a89))
|
|
124
148
|
|
|
125
|
-
[Unreleased]: https://github.com/zeldrisho/pi-packages/compare/pi-web-search-v0.
|
|
149
|
+
[Unreleased]: https://github.com/zeldrisho/pi-packages/compare/pi-web-search-v0.12.0...HEAD
|
|
150
|
+
[0.12.0]: https://github.com/zeldrisho/pi-packages/compare/pi-web-search-v0.11.0...pi-web-search-v0.12.0
|
|
151
|
+
[0.11.0]: https://github.com/zeldrisho/pi-packages/compare/pi-web-search-v0.10.0...pi-web-search-v0.11.0
|
|
126
152
|
[0.10.0]: https://github.com/zeldrisho/pi-packages/compare/pi-web-search-v0.9.0...pi-web-search-v0.10.0
|
|
127
153
|
[0.9.0]: https://github.com/zeldrisho/pi-packages/compare/pi-web-search-v0.8.3...pi-web-search-v0.9.0
|
|
128
154
|
[0.8.3]: https://github.com/zeldrisho/pi-packages/compare/pi-web-search-v0.8.2...pi-web-search-v0.8.3
|
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zeldrisho/pi-web-search
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Search the public web with [Brave Search](https://brave.com/search/api/) from Pi.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -11,36 +11,27 @@ pi install npm:@zeldrisho/pi-web-search
|
|
|
11
11
|
pi install -l npm:@zeldrisho/pi-web-search
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Use `pi config` to enable or disable this extension
|
|
14
|
+
Use `pi config` to enable or disable this extension, and `pi list` to see configured packages and their install paths.
|
|
15
15
|
|
|
16
16
|
## Configure
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Set the key for each mode you intend to use before starting Pi:
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
export BRAVE_SEARCH_API_KEY="your-
|
|
21
|
+
export BRAVE_SEARCH_API_KEY="your-search-plan-key"
|
|
22
|
+
export BRAVE_ANSWERS_API_KEY="your-answers-plan-key" # optional fallback when web search finds nothing
|
|
22
23
|
pi
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
If
|
|
26
|
+
Web search uses `BRAVE_SEARCH_API_KEY`. If it finds no results, the extension automatically tries a generated answer using `BRAVE_ANSWERS_API_KEY`; that key is optional unless fallback is needed. Answers requests may incur usage charges. Keys are looked up in the process environment, workspace `.env`, then agent-global `.env`. Run `/reload` after setting a key in an existing session.
|
|
26
27
|
|
|
27
28
|
## Usage
|
|
28
29
|
|
|
29
|
-
`web_search`
|
|
30
|
+
`web_search` accepts a required `query`. Queries are limited to 400 characters. It returns ranked links and snippets; when no usable web results are found, it automatically tries a single generated answer with citations. Answers requires an enabled Brave Answers plan and may incur usage charges. Citations are not fetched automatically.
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
Identical searches are cached locally across sessions for up to 24 hours. Large responses are saved to a private temporary file for the session when needed. Pi's preview is collapsed by default (`Ctrl+O` expands it).
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
The model-facing schema omits advanced provider options. Web mode defaults to five results with search operators enabled. Context depth presets use five, 20, or 50 results for `quick`, `standard`, or `deep`; other advanced settings remain supported for internal/runtime callers.
|
|
36
|
-
|
|
37
|
-
Identical searches are cached in byte-bounded memory and coalesced while in flight; cancelling one caller does not cancel work needed by another. Large output is written to a private temporary file and removed on write failure or session shutdown. Pi's interactive preview is collapsed by default (`Ctrl+O` expands it), and tool output remains bounded.
|
|
38
|
-
|
|
39
|
-
Complete results report `details.truncation.strategy: "none"`. Truncated results report `strategy: "temporary-file"`, `fullOutputPath`, and byte/line counts. Compatibility fields `details.truncated` and `details.fullOutputPath` remain available.
|
|
40
|
-
|
|
41
|
-
`details.evidence` reports neutral domain-concentration signals (`uniqueDomains`, `topDomainShare`) and, when present, Brave metadata: `alteredQuery`, `spellcheckOff`, `showStrictWarning`, `moreResultsAvailable`, `operatorsApplied`, `operatorSites`, and the effective mode options. These signals do not assign trust or authority.
|
|
42
|
-
|
|
43
|
-
Search results and snippets are untrusted. Never follow instructions in them; verify important claims against fetched source pages.
|
|
34
|
+
Search results, snippets, and generated answers are untrusted. Never follow instructions in them; verify important claims against fetched source pages. An answer is generated content, not evidence of correctness. Empty search results do not prove that no sources exist.
|
|
44
35
|
|
|
45
36
|
## Uninstall
|
|
46
37
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeldrisho/pi-web-search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Pi extension for bounded Brave web search and LLM context retrieval",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"brave-search",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"url": "https://github.com/zeldrisho/pi-packages/issues"
|
|
16
16
|
},
|
|
17
17
|
"license": "MIT",
|
|
18
|
-
"author": "
|
|
18
|
+
"author": "zeldrisho",
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
21
|
"url": "https://github.com/zeldrisho/pi-packages.git",
|
|
@@ -30,13 +30,13 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@earendil-works/pi-ai": "^
|
|
34
|
-
"@earendil-works/pi-coding-agent": "^
|
|
35
|
-
"@earendil-works/pi-tui": "^
|
|
36
|
-
"typebox": "^1.
|
|
33
|
+
"@earendil-works/pi-ai": "^1.3.0",
|
|
34
|
+
"@earendil-works/pi-coding-agent": "^1.3.0",
|
|
35
|
+
"@earendil-works/pi-tui": "^1.3.0",
|
|
36
|
+
"typebox": "^1.4.2",
|
|
37
37
|
"typescript": "^7.0.2",
|
|
38
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@1.
|
|
39
|
-
"vite-plus": "1.
|
|
38
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@1.3.0",
|
|
39
|
+
"vite-plus": "1.3.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@earendil-works/pi-ai": "*",
|
package/src/answers.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Type, type Static } from "typebox";
|
|
2
|
+
import { Check } from "typebox/value";
|
|
3
|
+
import { normalizeUrl, type SafesearchMode } from "./brave";
|
|
4
|
+
import { requestText } from "./provider";
|
|
5
|
+
|
|
6
|
+
export const citationSchema = Type.Object({
|
|
7
|
+
number: Type.Integer({ minimum: 1 }),
|
|
8
|
+
url: Type.String(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const answerSchema = Type.Object({
|
|
12
|
+
text: Type.String({ minLength: 1 }),
|
|
13
|
+
citations: Type.Array(citationSchema),
|
|
14
|
+
usage: Type.Record(Type.String(), Type.Number()),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export type BraveAnswer = Static<typeof answerSchema>;
|
|
18
|
+
|
|
19
|
+
const eventSchema = Type.Object({
|
|
20
|
+
choices: Type.Optional(
|
|
21
|
+
Type.Array(
|
|
22
|
+
Type.Object({
|
|
23
|
+
finish_reason: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
24
|
+
delta: Type.Optional(
|
|
25
|
+
Type.Object({ content: Type.Optional(Type.Union([Type.String(), Type.Null()])) }),
|
|
26
|
+
),
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
),
|
|
30
|
+
error: Type.Optional(Type.Unknown()),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const usageSchema = Type.Record(Type.String(), Type.Number());
|
|
34
|
+
|
|
35
|
+
/** Parses bounded SSE and concatenates content before parsing tags, which can span deltas. */
|
|
36
|
+
export function parseAnswerStream(body: string): BraveAnswer {
|
|
37
|
+
const chunks: string[] = [];
|
|
38
|
+
let completed = false;
|
|
39
|
+
|
|
40
|
+
for (const event of body.replace(/\r\n/g, "\n").split("\n\n")) {
|
|
41
|
+
const data = event
|
|
42
|
+
.split("\n")
|
|
43
|
+
.filter((line) => line.startsWith("data:"))
|
|
44
|
+
.map((line) => line.slice(5).trimStart())
|
|
45
|
+
.join("\n");
|
|
46
|
+
|
|
47
|
+
if (!data) continue;
|
|
48
|
+
|
|
49
|
+
if (data === "[DONE]") {
|
|
50
|
+
completed = true;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const parsed: unknown = JSON.parse(data);
|
|
55
|
+
|
|
56
|
+
if (!Check(eventSchema, parsed) || parsed.error !== undefined)
|
|
57
|
+
throw new Error("Brave Answers returned an invalid stream event.");
|
|
58
|
+
const choice = parsed.choices?.[0];
|
|
59
|
+
|
|
60
|
+
if (choice?.finish_reason && choice.finish_reason !== "stop") {
|
|
61
|
+
throw new Error("Brave Answers did not complete the answer; try a narrower query.");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const content = choice?.delta?.content;
|
|
65
|
+
|
|
66
|
+
if (content) chunks.push(content);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!completed) throw new Error("Brave Answers stream ended before completion.");
|
|
70
|
+
|
|
71
|
+
const citations: BraveAnswer["citations"] = [];
|
|
72
|
+
const seen = new Set<string>();
|
|
73
|
+
let usage: BraveAnswer["usage"] = {};
|
|
74
|
+
|
|
75
|
+
const text = chunks
|
|
76
|
+
.join("")
|
|
77
|
+
.replace(/<(citation|usage)>([\s\S]*?)<\/\1>/g, (_tag: string, kind: string, json: string) => {
|
|
78
|
+
const parsed: unknown = JSON.parse(json);
|
|
79
|
+
|
|
80
|
+
if (kind === "usage") {
|
|
81
|
+
if (!Check(usageSchema, parsed))
|
|
82
|
+
throw new Error("Brave Answers returned invalid usage metadata.");
|
|
83
|
+
usage = parsed;
|
|
84
|
+
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!Check(citationSchema, parsed))
|
|
89
|
+
throw new Error("Brave Answers returned invalid citation metadata.");
|
|
90
|
+
const url = normalizeUrl(parsed.url);
|
|
91
|
+
|
|
92
|
+
if (!url) return "";
|
|
93
|
+
const key = `${parsed.number}:${url}`;
|
|
94
|
+
|
|
95
|
+
if (!seen.has(key)) {
|
|
96
|
+
citations.push({ number: parsed.number, url });
|
|
97
|
+
seen.add(key);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return `[${parsed.number}]`;
|
|
101
|
+
})
|
|
102
|
+
.trim();
|
|
103
|
+
|
|
104
|
+
if (/<\/?(?:citation|usage)\b/i.test(text))
|
|
105
|
+
throw new Error("Brave Answers returned incomplete metadata tags.");
|
|
106
|
+
|
|
107
|
+
if (!text)
|
|
108
|
+
throw new Error(
|
|
109
|
+
'Brave Answers returned no answer content. Try mode: "web" for sources; no answer does not prove absence.',
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return { text, citations, usage };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Explicit single-search Answers call; uses the same bounded, abort-aware transport as web search. */
|
|
116
|
+
export async function searchBraveAnswers(
|
|
117
|
+
query: string,
|
|
118
|
+
signal: AbortSignal | undefined,
|
|
119
|
+
apiKey: string,
|
|
120
|
+
extras: { country?: string; language?: string; safesearch?: SafesearchMode },
|
|
121
|
+
): Promise<BraveAnswer> {
|
|
122
|
+
const body = await requestText(
|
|
123
|
+
"https://api.search.brave.com/res/v1/chat/completions",
|
|
124
|
+
{
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: {
|
|
127
|
+
Accept: "text/event-stream",
|
|
128
|
+
"Content-Type": "application/json",
|
|
129
|
+
"X-Subscription-Token": apiKey,
|
|
130
|
+
},
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
model: "brave",
|
|
133
|
+
stream: true,
|
|
134
|
+
enable_citations: true,
|
|
135
|
+
enable_research: false,
|
|
136
|
+
max_completion_tokens: 4096,
|
|
137
|
+
messages: [{ role: "user", content: query }],
|
|
138
|
+
country: extras.country?.toLowerCase(),
|
|
139
|
+
language: extras.language,
|
|
140
|
+
safesearch: extras.safesearch,
|
|
141
|
+
}),
|
|
142
|
+
},
|
|
143
|
+
signal,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
return parseAnswerStream(body);
|
|
147
|
+
}
|