@demigodmode/pi-web-agent 1.4.0 → 1.5.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/CHANGELOG.md +43 -13
- package/README.md +5 -5
- package/dist/backends/config.d.ts +1 -1
- package/dist/backends/config.js +11 -7
- package/dist/backends/doctor.js +39 -0
- package/dist/backends/factory.d.ts +2 -0
- package/dist/backends/factory.js +12 -5
- package/dist/commands/web-agent-config.js +18 -8
- package/dist/search/brave.d.ts +7 -0
- package/dist/search/brave.js +83 -0
- package/dist/types.d.ts +2 -2
- package/package.json +8 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,13 +7,43 @@ The format is intentionally simple and release-oriented.
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
|
-
-
|
|
10
|
+
- None.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- None.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- None.
|
|
17
|
+
|
|
18
|
+
### Breaking
|
|
19
|
+
- None.
|
|
20
|
+
|
|
21
|
+
## [1.5.1] - 2026-06-22
|
|
22
|
+
### Added
|
|
23
|
+
- None.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- None.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- Updated vulnerable dependency paths reported by `npm audit`, including Vitest, Pi dev tooling, `undici`, `ws`, `protobufjs`, `postcss`, and the Vite docs toolchain override.
|
|
30
|
+
|
|
31
|
+
### Breaking
|
|
32
|
+
- None.
|
|
33
|
+
|
|
34
|
+
## [1.5.0] - 2026-06-16
|
|
35
|
+
### Added
|
|
36
|
+
- Added Brave Search as the first hosted search backend for `web_explore`, giving users a high-quality API-backed discovery option without running their own SearXNG instance.
|
|
37
|
+
- Added optional Brave → DuckDuckGo fallback so hosted search can degrade gracefully instead of failing hard when the Brave API is unavailable or misconfigured.
|
|
38
|
+
- Added a dedicated Brave search adapter that feeds the existing `web_explore` research pipeline, so Brave improves source discovery while the package still handles fetching, ranking, evidence quality, synthesis, and caveats.
|
|
11
39
|
|
|
12
40
|
### Changed
|
|
13
|
-
-
|
|
41
|
+
- `/web-agent settings` now treats Brave as a first-class search backend while keeping API keys out of config files; users configure `PI_WEB_AGENT_BRAVE_API_KEY` in their environment instead.
|
|
42
|
+
- `/web-agent doctor` now reports Brave setup status, warns when `PI_WEB_AGENT_BRAVE_API_KEY` is missing, and validates configured Brave access when a key is present.
|
|
43
|
+
- Backend config validation and fallback metadata now understand Brave, including clear `fallbackFrom: 'brave'` reporting when DuckDuckGo fallback is used.
|
|
14
44
|
|
|
15
45
|
### Fixed
|
|
16
|
-
-
|
|
46
|
+
- None.
|
|
17
47
|
|
|
18
48
|
### Breaking
|
|
19
49
|
- None.
|
|
@@ -27,7 +57,7 @@ The format is intentionally simple and release-oriented.
|
|
|
27
57
|
- Partial research caveats are now more specific when evidence is community-only, low-diversity, blocked, or cautionary.
|
|
28
58
|
|
|
29
59
|
### Fixed
|
|
30
|
-
-
|
|
60
|
+
- None.
|
|
31
61
|
|
|
32
62
|
### Breaking
|
|
33
63
|
- None.
|
|
@@ -54,10 +84,10 @@ The format is intentionally simple and release-oriented.
|
|
|
54
84
|
- Added interactive URL prompts for SearXNG and Firecrawl backend setup.
|
|
55
85
|
|
|
56
86
|
### Changed
|
|
57
|
-
-
|
|
87
|
+
- None.
|
|
58
88
|
|
|
59
89
|
### Fixed
|
|
60
|
-
-
|
|
90
|
+
- None.
|
|
61
91
|
|
|
62
92
|
### Breaking
|
|
63
93
|
- None.
|
|
@@ -72,7 +102,7 @@ The format is intentionally simple and release-oriented.
|
|
|
72
102
|
- `/web-agent show` and `/web-agent doctor` now report configured backend fallback/options.
|
|
73
103
|
|
|
74
104
|
### Fixed
|
|
75
|
-
-
|
|
105
|
+
- None.
|
|
76
106
|
|
|
77
107
|
### Breaking
|
|
78
108
|
- None.
|
|
@@ -85,7 +115,7 @@ The format is intentionally simple and release-oriented.
|
|
|
85
115
|
- Migrated Pi package imports to `@earendil-works/*` after the upstream Pi scope move.
|
|
86
116
|
|
|
87
117
|
### Fixed
|
|
88
|
-
-
|
|
118
|
+
- None.
|
|
89
119
|
|
|
90
120
|
### Breaking
|
|
91
121
|
- This release requires Pi 0.74+. Users on older Pi versions should stay on `@demigodmode/pi-web-agent@0.6.x` until they update Pi.
|
|
@@ -109,10 +139,10 @@ The format is intentionally simple and release-oriented.
|
|
|
109
139
|
|
|
110
140
|
## [0.5.1] - 2026-05-04
|
|
111
141
|
### Added
|
|
112
|
-
-
|
|
142
|
+
- None.
|
|
113
143
|
|
|
114
144
|
### Changed
|
|
115
|
-
-
|
|
145
|
+
- None.
|
|
116
146
|
|
|
117
147
|
### Fixed
|
|
118
148
|
- Fixed the Windows browser-resolution test so it is deterministic on Linux CI.
|
|
@@ -157,7 +187,7 @@ The format is intentionally simple and release-oriented.
|
|
|
157
187
|
|
|
158
188
|
## [0.3.1] - 2026-04-22
|
|
159
189
|
### Added
|
|
160
|
-
-
|
|
190
|
+
- None.
|
|
161
191
|
|
|
162
192
|
### Changed
|
|
163
193
|
- Stopped self-upgrading npm inside the publish workflow before install and publish steps.
|
|
@@ -205,10 +235,10 @@ The format is intentionally simple and release-oriented.
|
|
|
205
235
|
|
|
206
236
|
## [0.2.1] - 2026-04-20
|
|
207
237
|
### Added
|
|
208
|
-
-
|
|
238
|
+
- None.
|
|
209
239
|
|
|
210
240
|
### Changed
|
|
211
|
-
-
|
|
241
|
+
- None.
|
|
212
242
|
|
|
213
243
|
### Fixed
|
|
214
244
|
- Added the missing npm `--provenance` flag to the publish workflow so Trusted Publishing can exchange the GitHub OIDC token correctly.
|
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
Most agent web tools blur search, fetch, browser rendering, and research into one vague thing. `pi-web-agent` exposes one public research tool, `web_explore`, and keeps search/fetch/headless work inside that bounded workflow.
|
|
10
10
|
|
|
11
|
-
The point is keeping the model-facing boundary simple: ask `web_explore` to research a question, and it handles direct links, discovery, HTTP reads, targeted browser rendering, source ranking, and caveats internally.
|
|
11
|
+
The point is keeping the model-facing boundary simple: ask `web_explore` to research a question, and it handles direct links, discovery, HTTP reads, targeted browser rendering, source ranking, source-quality checks, and caveats internally.
|
|
12
12
|
|
|
13
|
-
That sounds obvious, but a lot of agent tooling gets fuzzy right there. This package is meant to be stricter about what it actually did and more willing to say when a read was not good enough to trust.
|
|
13
|
+
That sounds obvious, but a lot of agent tooling gets fuzzy right there. This package is meant to be stricter about what it actually did and more willing to say when a read was not good enough to trust. Bot-check pages, narrow source sets, unreadable threads, and cautionary/conflicting evidence should show up as caveats instead of fake confidence.
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
@@ -109,7 +109,7 @@ Example:
|
|
|
109
109
|
}
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
Backend config is also supported. Defaults remain DuckDuckGo search, plain HTTP fetch, and local-browser headless fallback with managed Chromium fallback configured.
|
|
112
|
+
Backend config is also supported. Defaults remain DuckDuckGo search, plain HTTP fetch, and local-browser headless fallback with managed Chromium fallback configured. If you have a Brave Search API key, Brave can be selected as a hosted search backend while `web_explore` still handles page reading, ranking, and caveats itself.
|
|
113
113
|
|
|
114
114
|
Backend settings can be changed from:
|
|
115
115
|
|
|
@@ -117,9 +117,9 @@ Backend settings can be changed from:
|
|
|
117
117
|
/web-agent settings
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
Choose **Backends** to edit search/fetch providers, fallback behavior, and SearXNG or Firecrawl base URLs interactively. Firecrawl API keys should stay in environment variables rather than being written into config files.
|
|
120
|
+
Choose **Backends** to edit search/fetch providers, fallback behavior, and SearXNG or Firecrawl base URLs interactively. Brave Search uses `PI_WEB_AGENT_BRAVE_API_KEY`. Firecrawl API keys should also stay in environment variables rather than being written into config files.
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
For the full backend config shape, including SearXNG, Brave, Firecrawl, and fallback behavior, see:
|
|
123
123
|
|
|
124
124
|
- https://demigodmode.github.io/pi-web-agent/self-hosted-backends
|
|
125
125
|
|
|
@@ -8,7 +8,7 @@ export type FirecrawlOptions = {
|
|
|
8
8
|
onlyMainContent?: boolean;
|
|
9
9
|
};
|
|
10
10
|
export type SearchBackendConfig = {
|
|
11
|
-
provider: 'duckduckgo' | 'searxng';
|
|
11
|
+
provider: 'duckduckgo' | 'searxng' | 'brave';
|
|
12
12
|
baseUrl?: string;
|
|
13
13
|
fallback?: 'duckduckgo';
|
|
14
14
|
options?: SearxngOptions;
|
package/dist/backends/config.js
CHANGED
|
@@ -38,17 +38,21 @@ function extractFirecrawlOptions(value) {
|
|
|
38
38
|
export function extractBackendConfigOverride(file) {
|
|
39
39
|
const backends = file?.backends;
|
|
40
40
|
const override = {};
|
|
41
|
-
if (backends?.search?.provider === 'duckduckgo' ||
|
|
41
|
+
if (backends?.search?.provider === 'duckduckgo' ||
|
|
42
|
+
backends?.search?.provider === 'searxng' ||
|
|
43
|
+
backends?.search?.provider === 'brave') {
|
|
42
44
|
override.search = { provider: backends.search.provider };
|
|
43
|
-
if (typeof backends.search.baseUrl === 'string') {
|
|
45
|
+
if (backends.search.provider === 'searxng' && typeof backends.search.baseUrl === 'string') {
|
|
44
46
|
override.search.baseUrl = backends.search.baseUrl;
|
|
45
47
|
}
|
|
46
48
|
if (backends.search.fallback === 'duckduckgo') {
|
|
47
49
|
override.search.fallback = 'duckduckgo';
|
|
48
50
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
if (backends.search.provider === 'searxng') {
|
|
52
|
+
const options = extractSearxngOptions(backends.search.options);
|
|
53
|
+
if (options) {
|
|
54
|
+
override.search.options = options;
|
|
55
|
+
}
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
if (backends?.fetch?.provider === 'http' || backends?.fetch?.provider === 'firecrawl') {
|
|
@@ -80,8 +84,8 @@ export function validateBackendConfig(config) {
|
|
|
80
84
|
if (config.fetch.provider === 'firecrawl' && !config.fetch.baseUrl) {
|
|
81
85
|
issues.push('fetch provider firecrawl requires backends.fetch.baseUrl');
|
|
82
86
|
}
|
|
83
|
-
if (config.search.fallback === 'duckduckgo' && config.search.provider !== 'searxng') {
|
|
84
|
-
issues.push('search fallback duckduckgo is only supported when search provider is searxng');
|
|
87
|
+
if (config.search.fallback === 'duckduckgo' && config.search.provider !== 'searxng' && config.search.provider !== 'brave') {
|
|
88
|
+
issues.push('search fallback duckduckgo is only supported when search provider is searxng or brave');
|
|
85
89
|
}
|
|
86
90
|
if (config.fetch.fallback === 'http' && config.fetch.provider !== 'firecrawl') {
|
|
87
91
|
issues.push('fetch fallback http is only supported when fetch provider is firecrawl');
|
package/dist/backends/doctor.js
CHANGED
|
@@ -6,6 +6,12 @@ function withTimeout(timeoutMs) {
|
|
|
6
6
|
function message(error) {
|
|
7
7
|
return error instanceof Error ? error.message : String(error);
|
|
8
8
|
}
|
|
9
|
+
function braveDoctorUrl() {
|
|
10
|
+
const url = new URL('https://api.search.brave.com/res/v1/web/search');
|
|
11
|
+
url.searchParams.set('q', 'pi-web-agent-doctor');
|
|
12
|
+
url.searchParams.set('count', '1');
|
|
13
|
+
return url.toString();
|
|
14
|
+
}
|
|
9
15
|
function searxngDoctorUrl(baseUrl, options = {}) {
|
|
10
16
|
const url = new URL('/search', baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);
|
|
11
17
|
url.searchParams.set('q', 'pi-web-agent-doctor');
|
|
@@ -30,6 +36,39 @@ export async function checkBackendHealth(config, { fetchImpl = fetch, timeoutMs
|
|
|
30
36
|
if (config.search.provider === 'duckduckgo') {
|
|
31
37
|
lines.push('search backend: duckduckgo');
|
|
32
38
|
}
|
|
39
|
+
else if (config.search.provider === 'brave') {
|
|
40
|
+
const apiKey = process.env.PI_WEB_AGENT_BRAVE_API_KEY;
|
|
41
|
+
if (!apiKey?.trim()) {
|
|
42
|
+
lines.push('search backend: brave warning (missing PI_WEB_AGENT_BRAVE_API_KEY)');
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const timeout = withTimeout(timeoutMs);
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetchImpl(braveDoctorUrl(), {
|
|
48
|
+
headers: {
|
|
49
|
+
Accept: 'application/json',
|
|
50
|
+
'X-Subscription-Token': apiKey
|
|
51
|
+
},
|
|
52
|
+
signal: timeout.signal
|
|
53
|
+
});
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
lines.push(`search backend: brave warning (HTTP ${response.status})`);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
const json = (await response.json());
|
|
59
|
+
lines.push(Array.isArray(json.web?.results)
|
|
60
|
+
? 'search backend: brave ok'
|
|
61
|
+
: 'search backend: brave warning (unexpected response)');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
lines.push(`search backend: brave warning (${message(error)})`);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
timeout.done();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
33
72
|
else if (!config.search.baseUrl) {
|
|
34
73
|
lines.push('search backend: searxng warning (missing baseUrl)');
|
|
35
74
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createFirecrawlFetcher } from '../fetch/firecrawl-fetch.js';
|
|
2
|
+
import { createBraveSearchTool } from '../search/brave.js';
|
|
2
3
|
import { createSearxngSearchTool } from '../search/searxng.js';
|
|
3
4
|
import { createWebFetchHeadlessTool } from '../tools/web-fetch-headless.js';
|
|
4
5
|
import { createWebFetchTool } from '../tools/web-fetch.js';
|
|
@@ -19,6 +20,7 @@ export type BackendSet = {
|
|
|
19
20
|
export type BackendFactoryDeps = {
|
|
20
21
|
createDuckDuckGoSearch?: typeof createWebSearchTool;
|
|
21
22
|
createSearxngSearch?: typeof createSearxngSearchTool;
|
|
23
|
+
createBraveSearch?: typeof createBraveSearchTool;
|
|
22
24
|
createHttpFetch?: typeof createWebFetchTool;
|
|
23
25
|
createFirecrawlFetch?: typeof createFirecrawlFetcher;
|
|
24
26
|
createHeadlessFetch?: typeof createWebFetchHeadlessTool;
|
package/dist/backends/factory.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createFirecrawlFetcher } from '../fetch/firecrawl-fetch.js';
|
|
2
|
+
import { createBraveSearchTool } from '../search/brave.js';
|
|
2
3
|
import { createSearxngSearchTool } from '../search/searxng.js';
|
|
3
4
|
import { buildFetchPresentation } from '../presentation/fetch-presentation.js';
|
|
4
5
|
import { buildSearchPresentation } from '../presentation/search-presentation.js';
|
|
@@ -34,7 +35,7 @@ function invalidFirecrawlFetch() {
|
|
|
34
35
|
return { ...result, presentation: buildFetchPresentation(result) };
|
|
35
36
|
};
|
|
36
37
|
}
|
|
37
|
-
function withSearchFallback(primary, fallback) {
|
|
38
|
+
function withSearchFallback(primary, fallback, fallbackFrom) {
|
|
38
39
|
return async (input) => {
|
|
39
40
|
const first = await primary(input);
|
|
40
41
|
if (first.status !== 'error')
|
|
@@ -44,8 +45,8 @@ function withSearchFallback(primary, fallback) {
|
|
|
44
45
|
...second,
|
|
45
46
|
metadata: {
|
|
46
47
|
...second.metadata,
|
|
47
|
-
fallbackFrom
|
|
48
|
-
fallbackReason: first.error?.message ??
|
|
48
|
+
fallbackFrom,
|
|
49
|
+
fallbackReason: first.error?.message ?? `${fallbackFrom} search failed.`
|
|
49
50
|
}
|
|
50
51
|
};
|
|
51
52
|
return { ...result, presentation: buildSearchPresentation(result) };
|
|
@@ -71,6 +72,7 @@ function withFetchFallback(primary, fallback) {
|
|
|
71
72
|
export function createBackendSet(config = DEFAULT_BACKEND_CONFIG, deps = {}) {
|
|
72
73
|
const createDuckDuckGoSearch = deps.createDuckDuckGoSearch ?? createWebSearchTool;
|
|
73
74
|
const createSearxngSearch = deps.createSearxngSearch ?? createSearxngSearchTool;
|
|
75
|
+
const createBraveSearch = deps.createBraveSearch ?? createBraveSearchTool;
|
|
74
76
|
const createHttpFetch = deps.createHttpFetch ?? createWebFetchTool;
|
|
75
77
|
const createFirecrawlFetch = deps.createFirecrawlFetch ?? createFirecrawlFetcher;
|
|
76
78
|
const createHeadlessFetch = deps.createHeadlessFetch ?? createWebFetchHeadlessTool;
|
|
@@ -78,9 +80,14 @@ export function createBackendSet(config = DEFAULT_BACKEND_CONFIG, deps = {}) {
|
|
|
78
80
|
? config.search.baseUrl
|
|
79
81
|
? createSearxngSearch({ baseUrl: config.search.baseUrl, options: config.search.options })
|
|
80
82
|
: invalidSearxngSearch()
|
|
81
|
-
:
|
|
83
|
+
: config.search.provider === 'brave'
|
|
84
|
+
? createBraveSearch({ apiKey: process.env.PI_WEB_AGENT_BRAVE_API_KEY })
|
|
85
|
+
: createDuckDuckGoSearch();
|
|
82
86
|
if (config.search.provider === 'searxng' && config.search.fallback === 'duckduckgo') {
|
|
83
|
-
search = withSearchFallback(search, createDuckDuckGoSearch());
|
|
87
|
+
search = withSearchFallback(search, createDuckDuckGoSearch(), 'searxng');
|
|
88
|
+
}
|
|
89
|
+
if (config.search.provider === 'brave' && config.search.fallback === 'duckduckgo') {
|
|
90
|
+
search = withSearchFallback(search, createDuckDuckGoSearch(), 'brave');
|
|
84
91
|
}
|
|
85
92
|
const httpFetch = createHttpFetch();
|
|
86
93
|
let fetchPage = config.fetch.provider === 'firecrawl'
|
|
@@ -124,7 +124,7 @@ function buildBackendSettingsItems(scope, backends) {
|
|
|
124
124
|
id: 'backend:search:provider',
|
|
125
125
|
label: 'Search backend',
|
|
126
126
|
currentValue: backends.search.provider,
|
|
127
|
-
values: ['duckduckgo', 'searxng']
|
|
127
|
+
values: ['duckduckgo', 'searxng', 'brave']
|
|
128
128
|
},
|
|
129
129
|
{
|
|
130
130
|
id: 'backend:search:baseUrl',
|
|
@@ -134,9 +134,15 @@ function buildBackendSettingsItems(scope, backends) {
|
|
|
134
134
|
},
|
|
135
135
|
{
|
|
136
136
|
id: 'backend:search:fallback',
|
|
137
|
-
label: '
|
|
138
|
-
currentValue: backends.search.provider === 'searxng' ? backends.search.fallback ?? 'off' : 'off',
|
|
139
|
-
values: backends.search.provider === 'searxng' ? ['off', 'duckduckgo'] : ['off']
|
|
137
|
+
label: 'Search fallback',
|
|
138
|
+
currentValue: backends.search.provider === 'searxng' || backends.search.provider === 'brave' ? backends.search.fallback ?? 'off' : 'off',
|
|
139
|
+
values: backends.search.provider === 'searxng' || backends.search.provider === 'brave' ? ['off', 'duckduckgo'] : ['off']
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: 'backend:secret:brave',
|
|
143
|
+
label: 'Brave API key',
|
|
144
|
+
currentValue: 'env var',
|
|
145
|
+
values: ['env var']
|
|
140
146
|
},
|
|
141
147
|
{
|
|
142
148
|
id: 'backend:fetch:provider',
|
|
@@ -249,22 +255,26 @@ export function applySettingsValue(state, id, newValue) {
|
|
|
249
255
|
}
|
|
250
256
|
currentDraft.tools = nextTools;
|
|
251
257
|
}
|
|
252
|
-
if (id === 'backend:search:provider' && (newValue === 'duckduckgo' || newValue === 'searxng')) {
|
|
258
|
+
if (id === 'backend:search:provider' && (newValue === 'duckduckgo' || newValue === 'searxng' || newValue === 'brave')) {
|
|
253
259
|
currentBackends.search.provider = newValue;
|
|
260
|
+
if (newValue !== 'searxng') {
|
|
261
|
+
delete currentBackends.search.baseUrl;
|
|
262
|
+
delete currentBackends.search.options;
|
|
263
|
+
}
|
|
254
264
|
if (newValue === 'duckduckgo') {
|
|
255
265
|
delete currentBackends.search.fallback;
|
|
256
266
|
}
|
|
257
267
|
}
|
|
258
268
|
if (id === 'backend:search:fallback') {
|
|
259
|
-
if (newValue === 'duckduckgo' && currentBackends.search.provider === 'searxng') {
|
|
269
|
+
if (newValue === 'duckduckgo' && (currentBackends.search.provider === 'searxng' || currentBackends.search.provider === 'brave')) {
|
|
260
270
|
currentBackends.search.fallback = 'duckduckgo';
|
|
261
271
|
}
|
|
262
|
-
else
|
|
272
|
+
else {
|
|
263
273
|
delete currentBackends.search.fallback;
|
|
264
274
|
}
|
|
265
275
|
}
|
|
266
276
|
if (id === 'backend:search:baseUrl') {
|
|
267
|
-
if (newValue.trim()) {
|
|
277
|
+
if (currentBackends.search.provider === 'searxng' && newValue.trim()) {
|
|
268
278
|
currentBackends.search.baseUrl = newValue.trim();
|
|
269
279
|
}
|
|
270
280
|
else {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { buildSearchPresentation } from '../presentation/search-presentation.js';
|
|
2
|
+
const BRAVE_WEB_SEARCH_URL = 'https://api.search.brave.com/res/v1/web/search';
|
|
3
|
+
function resultWithPresentation(result) {
|
|
4
|
+
return { ...result, presentation: buildSearchPresentation(result) };
|
|
5
|
+
}
|
|
6
|
+
function normalizeResults(response) {
|
|
7
|
+
return (response.web?.results ?? []).flatMap((item) => {
|
|
8
|
+
if (typeof item.title !== 'string' || typeof item.url !== 'string') {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
title: item.title,
|
|
14
|
+
url: item.url,
|
|
15
|
+
snippet: typeof item.description === 'string' ? item.description : ''
|
|
16
|
+
}
|
|
17
|
+
];
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function buildBraveUrl(query) {
|
|
21
|
+
const url = new URL(BRAVE_WEB_SEARCH_URL);
|
|
22
|
+
url.searchParams.set('q', query);
|
|
23
|
+
return url.toString();
|
|
24
|
+
}
|
|
25
|
+
export function createBraveSearchTool({ apiKey, fetchImpl = fetch }) {
|
|
26
|
+
return async function braveSearch({ query }) {
|
|
27
|
+
const normalizedQuery = query.trim();
|
|
28
|
+
if (!normalizedQuery) {
|
|
29
|
+
return resultWithPresentation({
|
|
30
|
+
status: 'error',
|
|
31
|
+
results: [],
|
|
32
|
+
metadata: { backend: 'brave', cacheHit: false },
|
|
33
|
+
error: { code: 'INVALID_QUERY', message: 'Query must not be empty.' }
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (!apiKey?.trim()) {
|
|
37
|
+
return resultWithPresentation({
|
|
38
|
+
status: 'error',
|
|
39
|
+
results: [],
|
|
40
|
+
metadata: { backend: 'brave', cacheHit: false },
|
|
41
|
+
error: {
|
|
42
|
+
code: 'BACKEND_CONFIG_INVALID',
|
|
43
|
+
message: 'Brave search requires PI_WEB_AGENT_BRAVE_API_KEY.'
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetchImpl(buildBraveUrl(normalizedQuery), {
|
|
49
|
+
headers: {
|
|
50
|
+
Accept: 'application/json',
|
|
51
|
+
'X-Subscription-Token': apiKey
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
throw new Error(`HTTP ${response.status}`);
|
|
56
|
+
}
|
|
57
|
+
const parsed = (await response.json());
|
|
58
|
+
const results = normalizeResults(parsed);
|
|
59
|
+
if (results.length === 0) {
|
|
60
|
+
return resultWithPresentation({
|
|
61
|
+
status: 'error',
|
|
62
|
+
results: [],
|
|
63
|
+
metadata: { backend: 'brave', cacheHit: false },
|
|
64
|
+
error: { code: 'NO_RESULTS', message: 'Brave returned no usable results for this query.' }
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return resultWithPresentation({
|
|
68
|
+
status: 'ok',
|
|
69
|
+
results,
|
|
70
|
+
metadata: { backend: 'brave', cacheHit: false }
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
75
|
+
return resultWithPresentation({
|
|
76
|
+
status: 'error',
|
|
77
|
+
results: [],
|
|
78
|
+
metadata: { backend: 'brave', cacheHit: false },
|
|
79
|
+
error: { code: 'FETCH_FAILED', message: `Brave search request failed: ${rawMessage}` }
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -11,9 +11,9 @@ export type ToolError = {
|
|
|
11
11
|
message: string;
|
|
12
12
|
};
|
|
13
13
|
export type SearchMetadata = {
|
|
14
|
-
backend: 'duckduckgo' | 'searxng';
|
|
14
|
+
backend: 'duckduckgo' | 'searxng' | 'brave';
|
|
15
15
|
cacheHit: boolean;
|
|
16
|
-
fallbackFrom?: 'searxng';
|
|
16
|
+
fallbackFrom?: 'searxng' | 'brave';
|
|
17
17
|
fallbackReason?: string;
|
|
18
18
|
};
|
|
19
19
|
export type FetchMetadata = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@demigodmode/pi-web-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Pi package for reliable web access with explicit search, fetch, and headless boundaries.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/extension.js",
|
|
@@ -61,17 +61,20 @@
|
|
|
61
61
|
"typebox": "^1.1.37"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
65
|
-
"@earendil-works/pi-tui": "^0.
|
|
64
|
+
"@earendil-works/pi-coding-agent": "^0.79.10",
|
|
65
|
+
"@earendil-works/pi-tui": "^0.79.10",
|
|
66
66
|
"@types/jsdom": "^21.1.7",
|
|
67
67
|
"@types/node": "^24.0.0",
|
|
68
|
-
"@vitest/coverage-v8": "^3.2.
|
|
68
|
+
"@vitest/coverage-v8": "^3.2.6",
|
|
69
69
|
"typescript": "^5.8.0",
|
|
70
70
|
"vitepress": "^1.6.4",
|
|
71
|
-
"vitest": "^3.2.
|
|
71
|
+
"vitest": "^3.2.6"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
74
|
"@earendil-works/pi-coding-agent": "*",
|
|
75
75
|
"@earendil-works/pi-tui": "*"
|
|
76
|
+
},
|
|
77
|
+
"overrides": {
|
|
78
|
+
"vite": "6.4.3"
|
|
76
79
|
}
|
|
77
80
|
}
|