@demigodmode/pi-web-agent 1.9.0 → 1.11.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 +26 -0
- package/README.md +82 -79
- package/dist/backends/config.d.ts +27 -0
- package/dist/backends/config.js +97 -1
- package/dist/backends/factory.d.ts +2 -1
- package/dist/backends/factory.js +92 -23
- package/dist/commands/web-agent-config.d.ts +3 -0
- package/dist/commands/web-agent-config.js +57 -5
- package/dist/extension.d.ts +1 -0
- package/dist/extension.js +2 -0
- package/dist/fetch/headless-fetch.d.ts +8 -1
- package/dist/fetch/headless-fetch.js +3 -3
- package/dist/fetch/proxy-fetch.d.ts +22 -0
- package/dist/fetch/proxy-fetch.js +46 -0
- package/dist/jiti-compat-run.d.ts +1 -0
- package/dist/jiti-compat-run.js +9 -0
- package/dist/jiti-compat.d.ts +32 -0
- package/dist/jiti-compat.js +215 -0
- package/dist/presentation/config-store.js +4 -0
- package/dist/readers/youtube-reader.d.ts +3 -1
- package/dist/readers/youtube-reader.js +11 -3
- package/dist/search/duckduckgo.d.ts +10 -1
- package/dist/search/duckduckgo.js +23 -5
- package/dist/search/tavily.d.ts +2 -1
- package/dist/search/tavily.js +5 -3
- package/dist/tools/web-search.js +21 -9
- package/dist/types.d.ts +1 -1
- package/package.json +2 -1
- package/scripts/patch-jiti-compat.mjs +52 -8
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,32 @@ The format is intentionally simple and release-oriented.
|
|
|
18
18
|
### Breaking
|
|
19
19
|
- None.
|
|
20
20
|
|
|
21
|
+
## [1.11.0] - 2026-09-16
|
|
22
|
+
### Added
|
|
23
|
+
- Optional proxy support. Point web_explore at an HTTP/HTTPS proxy and everything outbound goes through it: search, fetch, Firecrawl, the GitHub/PDF/YouTube readers, doctor health checks, and the headless browser. Off unless you set it, and nothing changes if you don't. Set it in Settings → Backends, and keep credentials in `PI_WEB_AGENT_PROXY_USERNAME` / `PI_WEB_AGENT_PROXY_PASSWORD` rather than in the URL. Thanks to @lo-tp for building this. (#50)
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- `/web-agent doctor` reports whether the jsdom compat patch is in place, and prints the command to reapply it if it isn't.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- Pi failing to start with `Cannot find module 'punycode/'` or `Set operation called on non-Set object`. The patch for this was only applied at install time, but every pi extension shares one node_modules tree, so installing or updating anything else quietly reverted it. It now reapplies every time the extension loads. Also fixes it picking the wrong copy of the dependency when the shared tree holds more than one. (#34)
|
|
30
|
+
|
|
31
|
+
### Breaking
|
|
32
|
+
- None.
|
|
33
|
+
|
|
34
|
+
## [1.10.0] - 2026-08-25
|
|
35
|
+
### Added
|
|
36
|
+
- The zero-config default has a safety net now. When DuckDuckGo blocks a search (which happens fast on VPS and datacenter IPs), web search falls back to Tavily's keyless endpoint, no account or API key needed, so a fresh install still returns results instead of an error. Set `PI_WEB_AGENT_DISABLE_KEYLESS_FALLBACK=1` if you'd rather it just fail. (#42)
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- The keyless DuckDuckGo default is a lot harder to bot-wall. It sends real browser headers and retries once when a page looks blocked, instead of going out as an obvious script and getting bounced on the first request. (#42)
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
- Blocked searches are labeled honestly. A bot-wall page comes back as "blocked" (which triggers the fallback) instead of "couldn't parse", including the 200-status pages that mix a no-results message with a bot-wall notice. (#42)
|
|
43
|
+
|
|
44
|
+
### Breaking
|
|
45
|
+
- None.
|
|
46
|
+
|
|
21
47
|
## [1.9.0] - 2026-08-15
|
|
22
48
|
### Added
|
|
23
49
|
- Search fanout. Ask a hard question and web_explore can now hit several of your configured search backends at once, dedupe the merged results, and rank the pages that more than one provider agreed on to the top. Off by default. Flip it to on or auto in Settings → Backends (auto only fans out when the first provider comes back thin, so easy queries stay cheap). Preview/verbose shows which providers ran and which got skipped. (#35)
|
package/README.md
CHANGED
|
@@ -1,131 +1,136 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/demigodmode/pi-web-agent/main/docs/public/pi-web-agent-banner.png" alt="pi-web-agent: bounded web research for Pi" width="100%">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# pi-web-agent
|
|
2
6
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://github.com/demigodmode/pi-web-agent/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/demigodmode/pi-web-agent/ci.yml?branch=main&style=flat-square&logo=github&label=CI" alt="CI"></a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/@demigodmode/pi-web-agent"><img src="https://img.shields.io/npm/v/@demigodmode/pi-web-agent?style=flat-square&logo=npm&logoColor=white&color=CB3837" alt="npm version"></a>
|
|
10
|
+
<a href="https://www.npmjs.com/package/@demigodmode/pi-web-agent"><img src="https://img.shields.io/npm/dm/@demigodmode/pi-web-agent?style=flat-square&color=0A7BBB&label=downloads" alt="npm downloads"></a>
|
|
11
|
+
<img src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-475569?style=flat-square" alt="Platform: macOS, Linux, Windows">
|
|
12
|
+
<a href="https://demigodmode.github.io/pi-web-agent/"><img src="https://img.shields.io/badge/docs-github%20pages-2088FF?style=flat-square&logo=readthedocs&logoColor=white" alt="Docs"></a>
|
|
13
|
+
<a href="https://github.com/demigodmode/pi-web-agent/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@demigodmode/pi-web-agent?style=flat-square&color=6F42C1" alt="License"></a>
|
|
14
|
+
</p>
|
|
6
15
|
|
|
7
|
-
|
|
16
|
+
<p align="center">
|
|
17
|
+
<img src="https://img.shields.io/badge/search%20backends-475569?style=flat-square" alt="Search backends">
|
|
18
|
+
<img src="https://img.shields.io/badge/DuckDuckGo-475569?style=flat-square&logo=duckduckgo&logoColor=white" alt="DuckDuckGo">
|
|
19
|
+
<img src="https://img.shields.io/badge/SearXNG-475569?style=flat-square&logo=searxng&logoColor=white" alt="SearXNG">
|
|
20
|
+
<img src="https://img.shields.io/badge/Brave-475569?style=flat-square&logo=brave&logoColor=white" alt="Brave Search">
|
|
21
|
+
<img src="https://img.shields.io/badge/You.com-475569?style=flat-square" alt="You.com">
|
|
22
|
+
<img src="https://img.shields.io/badge/Exa-475569?style=flat-square" alt="Exa">
|
|
23
|
+
<img src="https://img.shields.io/badge/Tavily-475569?style=flat-square" alt="Tavily">
|
|
24
|
+
</p>
|
|
8
25
|
|
|
9
|
-
|
|
26
|
+
One public tool, `web_explore`, that does bounded web research for Pi: search, fetch, targeted browser rendering, ranking, and honest caveats, all behind a single call.
|
|
10
27
|
|
|
11
|
-
|
|
28
|
+
> Most agent web tooling blurs search, fetch, rendering, and synthesis into one vague thing. `pi-web-agent` keeps that boundary simple, and it is stricter about what it actually did: bot-check pages, narrow source sets, unreadable threads, and conflicting evidence show up as caveats instead of fake confidence.
|
|
12
29
|
|
|
13
|
-
|
|
30
|
+
## What you get
|
|
14
31
|
|
|
15
|
-
|
|
32
|
+
- **One tool.** `web_explore` handles direct links, discovery, HTTP reads, targeted headless rendering, source ranking, source-quality checks, and caveats internally.
|
|
33
|
+
- **Reads the real content behind links.** Paste a GitHub, PDF, or YouTube URL and it pulls the actual thing (GitHub files/issues/PRs from the API, PDF text, YouTube transcripts), keyless. So "summarize this PDF" or "what does this repo do" works off the source, not the page shell.
|
|
34
|
+
- **Six search backends.** DuckDuckGo (keyless default), SearXNG, Brave, You.com, Exa, Tavily.
|
|
35
|
+
- **Optional search fanout.** Query several backends at once, dedupe, and rank pages that more than one provider agreed on to the top. Off by default; flip it to `on` or `auto`.
|
|
36
|
+
- **Honest by default.** Weak, narrow, blocked, or cautionary evidence gets flagged instead of dressed up as confidence.
|
|
37
|
+
- **Bounded output.** `compact` / `preview` / `verbose` transcript modes.
|
|
38
|
+
- **Zero-config to start.** Runs keyless out of the box (DuckDuckGo search, local browser, the built-in readers). Opt into hosted backends, fallback, search fanout, and per-tool output modes through config when you want more control.
|
|
16
39
|
|
|
17
|
-
|
|
40
|
+
## Why pi-web-agent
|
|
18
41
|
|
|
19
|
-
|
|
20
|
-
pi install npm:@demigodmode/pi-web-agent
|
|
21
|
-
```
|
|
42
|
+
Compared to other web tooling for agents:
|
|
22
43
|
|
|
23
|
-
|
|
44
|
+
- **Hands-off.** No curator, no browser windows to approve, no step that pops you out of your session. Ask `web_explore` once and the answer comes back cleanly. Nothing to babysit.
|
|
45
|
+
- **Keyless by default.** Search, page reads, and the GitHub/PDF/YouTube readers all work with no API keys. Add hosted providers only when you want them.
|
|
46
|
+
- **Bounded and honest.** Compact output by default, and it says when a read was not good enough instead of returning fake confidence.
|
|
24
47
|
|
|
25
|
-
|
|
48
|
+
## Install
|
|
26
49
|
|
|
27
|
-
|
|
50
|
+
> `pi-web-agent` requires Pi 0.74+ (Pi packages moved to the `@earendil-works/*` scope). Update Pi before updating this package. On older Pi, stay on `@demigodmode/pi-web-agent@0.6.x`.
|
|
28
51
|
|
|
29
52
|
```bash
|
|
30
|
-
pi
|
|
53
|
+
pi install npm:@demigodmode/pi-web-agent
|
|
31
54
|
```
|
|
32
55
|
|
|
33
|
-
|
|
56
|
+
Reload or restart Pi after installing, then:
|
|
34
57
|
|
|
35
|
-
|
|
58
|
+
```text
|
|
59
|
+
/web-agent doctor # check it loaded and show configured backends
|
|
60
|
+
/web-agent # action menu
|
|
61
|
+
```
|
|
36
62
|
|
|
37
|
-
|
|
63
|
+
Update later with `pi update --extensions`.
|
|
38
64
|
|
|
39
|
-
|
|
65
|
+
**Browser rendering:** headless first tries a detected Chromium-family browser (Chrome, Chromium, Edge, Brave). If none is found, it falls back to Playwright-managed Chromium. Firefox/Safari-only systems still get search and plain HTTP reads.
|
|
40
66
|
|
|
41
|
-
|
|
42
|
-
npm run docs:dev
|
|
43
|
-
```
|
|
67
|
+
## Usage
|
|
44
68
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
npm run docs:build
|
|
49
|
-
```
|
|
69
|
+
Ask `web_explore` a web question:
|
|
50
70
|
|
|
51
|
-
|
|
71
|
+
> Find current docs and discussions on configuring Vitest coverage with the v8 provider.
|
|
52
72
|
|
|
53
|
-
|
|
73
|
+
Or hand it a link to read:
|
|
54
74
|
|
|
55
|
-
|
|
56
|
-
- `preview` — slightly richer bounded view
|
|
57
|
-
- `verbose` — fuller bounded view
|
|
75
|
+
> Summarize this PDF: https://arxiv.org/pdf/1706.03762
|
|
58
76
|
|
|
59
|
-
|
|
77
|
+
If a pass comes back thin, call `web_explore` again with a narrower query.
|
|
60
78
|
|
|
61
|
-
|
|
79
|
+
## Backends
|
|
62
80
|
|
|
63
|
-
|
|
81
|
+
Defaults are DuckDuckGo search, plain HTTP fetch, and local-browser headless. Switch providers from `/web-agent settings → Backends`. API keys stay in environment variables, never in config files.
|
|
64
82
|
|
|
65
|
-
|
|
83
|
+
| Backend | Role | Enable with |
|
|
84
|
+
| --- | --- | --- |
|
|
85
|
+
| DuckDuckGo | search (default) | nothing, keyless |
|
|
86
|
+
| SearXNG | search (self-hosted) | base URL |
|
|
87
|
+
| Brave | search (hosted) | `PI_WEB_AGENT_BRAVE_API_KEY` |
|
|
88
|
+
| You.com | search (hosted) | `YDC_API_KEY` |
|
|
89
|
+
| Exa | search (hosted) | `EXA_API_KEY` |
|
|
90
|
+
| Tavily | search (hosted) | `TAVILY_API_KEY` |
|
|
91
|
+
| Firecrawl | fetch (self-hosted) | base URL + `PI_WEB_AGENT_FIRECRAWL_API_KEY` |
|
|
92
|
+
| GitHub reader | content | `GITHUB_TOKEN` (optional, raises the rate limit) |
|
|
66
93
|
|
|
67
|
-
|
|
68
|
-
/web-agent settings
|
|
69
|
-
```
|
|
94
|
+
Full config shape (fallback, SearXNG/Firecrawl options, fanout): see the [self-hosted backends docs](https://demigodmode.github.io/pi-web-agent/self-hosted-backends).
|
|
70
95
|
|
|
71
|
-
|
|
96
|
+
## Settings
|
|
72
97
|
|
|
73
98
|
```text
|
|
74
|
-
/web-agent
|
|
75
|
-
/web-agent
|
|
99
|
+
/web-agent settings # main UI
|
|
100
|
+
/web-agent doctor # health check
|
|
101
|
+
/web-agent show # effective config
|
|
76
102
|
/web-agent changelog
|
|
77
|
-
/web-agent
|
|
78
|
-
/web-agent reset global
|
|
79
|
-
/web-agent mode preview
|
|
80
|
-
/web-agent mode web_explore verbose
|
|
81
|
-
/web-agent mode web_explore inherit
|
|
103
|
+
/web-agent mode web_explore verbose # per-tool presentation mode
|
|
104
|
+
/web-agent reset project | global
|
|
82
105
|
```
|
|
83
106
|
|
|
84
|
-
Config
|
|
107
|
+
Config is JSON, and project config overrides global:
|
|
85
108
|
|
|
86
109
|
```text
|
|
87
110
|
Global: ~/.pi/agent/extensions/pi-web-agent/config.json
|
|
88
111
|
Project: .pi/extensions/pi-web-agent/config.json
|
|
89
112
|
```
|
|
90
113
|
|
|
91
|
-
Precedence:
|
|
92
|
-
|
|
93
|
-
- built-in defaults
|
|
94
|
-
- global config
|
|
95
|
-
- project config
|
|
96
|
-
|
|
97
|
-
Project config overrides global config.
|
|
98
|
-
|
|
99
|
-
Example:
|
|
100
|
-
|
|
101
114
|
```json
|
|
102
115
|
{
|
|
103
116
|
"presentation": {
|
|
104
117
|
"defaultMode": "compact",
|
|
105
|
-
"tools": {
|
|
106
|
-
"web_explore": { "mode": "verbose" }
|
|
107
|
-
}
|
|
118
|
+
"tools": { "web_explore": { "mode": "verbose" } }
|
|
108
119
|
}
|
|
109
120
|
}
|
|
110
121
|
```
|
|
111
122
|
|
|
112
|
-
|
|
123
|
+
Presentation modes:
|
|
113
124
|
|
|
114
|
-
|
|
125
|
+
- `compact`: short summary, the default everywhere
|
|
126
|
+
- `preview`: slightly richer bounded view
|
|
127
|
+
- `verbose`: fuller bounded view
|
|
115
128
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
```text
|
|
119
|
-
/web-agent settings
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
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`, You.com uses `YDC_API_KEY`, Exa uses `EXA_API_KEY`, and Tavily uses `TAVILY_API_KEY`. Firecrawl API keys should also stay in environment variables rather than being written into config files.
|
|
123
|
-
|
|
124
|
-
For the full backend config shape, including SearXNG, Brave, You.com, Firecrawl, and fallback behavior, see:
|
|
129
|
+
## Docs
|
|
125
130
|
|
|
126
|
-
|
|
131
|
+
Full docs: <https://demigodmode.github.io/pi-web-agent/>. Work on them locally with `npm run docs:dev`.
|
|
127
132
|
|
|
128
|
-
##
|
|
133
|
+
## Development
|
|
129
134
|
|
|
130
135
|
```bash
|
|
131
136
|
npm install
|
|
@@ -134,10 +139,8 @@ npm run lint
|
|
|
134
139
|
npm run build
|
|
135
140
|
```
|
|
136
141
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
If Pi is already running, use `/reload` after changes.
|
|
142
|
+
Local Pi work uses `.pi/extensions/pi-web-agent.ts`; run `/reload` after changes.
|
|
140
143
|
|
|
141
144
|
## License
|
|
142
145
|
|
|
143
|
-
AGPL-3.0-only. See
|
|
146
|
+
AGPL-3.0-only. See [LICENSE](LICENSE).
|
|
@@ -12,6 +12,11 @@ export type FirecrawlOptions = {
|
|
|
12
12
|
formats?: string[];
|
|
13
13
|
onlyMainContent?: boolean;
|
|
14
14
|
};
|
|
15
|
+
export type ProxyConfig = {
|
|
16
|
+
url: string;
|
|
17
|
+
username?: string;
|
|
18
|
+
password?: string;
|
|
19
|
+
};
|
|
15
20
|
export type SearchBackendConfig = {
|
|
16
21
|
provider: 'duckduckgo' | 'searxng' | 'brave' | 'youcom' | 'exa' | 'tavily';
|
|
17
22
|
baseUrl?: string;
|
|
@@ -33,11 +38,13 @@ export type BackendConfig = {
|
|
|
33
38
|
search: SearchBackendConfig;
|
|
34
39
|
fetch: FetchBackendConfig;
|
|
35
40
|
headless: HeadlessBackendConfig;
|
|
41
|
+
proxy?: ProxyConfig;
|
|
36
42
|
};
|
|
37
43
|
export type BackendConfigOverride = {
|
|
38
44
|
search?: Partial<SearchBackendConfig>;
|
|
39
45
|
fetch?: Partial<FetchBackendConfig>;
|
|
40
46
|
headless?: Partial<HeadlessBackendConfig>;
|
|
47
|
+
proxy?: ProxyConfig;
|
|
41
48
|
};
|
|
42
49
|
export type BackendConfigFile = {
|
|
43
50
|
backends?: {
|
|
@@ -58,9 +65,29 @@ export type BackendConfigFile = {
|
|
|
58
65
|
headless?: {
|
|
59
66
|
provider?: unknown;
|
|
60
67
|
};
|
|
68
|
+
proxy?: {
|
|
69
|
+
url?: unknown;
|
|
70
|
+
username?: unknown;
|
|
71
|
+
password?: unknown;
|
|
72
|
+
};
|
|
61
73
|
};
|
|
62
74
|
};
|
|
63
75
|
export declare const DEFAULT_BACKEND_CONFIG: BackendConfig;
|
|
76
|
+
/**
|
|
77
|
+
* Remove any credentials (user:pass@) embedded in a proxy URL. pi-web-agent
|
|
78
|
+
* never reads or sends credentials from the URL; proxy auth comes from
|
|
79
|
+
* backends.proxy.username/password or PI_WEB_AGENT_PROXY_USERNAME /
|
|
80
|
+
* PI_WEB_AGENT_PROXY_PASSWORD. Stripping them here keeps them out of logs,
|
|
81
|
+
* doctor output, the settings UI, and the proxy connection itself.
|
|
82
|
+
*/
|
|
83
|
+
export declare function stripProxyCredentials(url: string): string;
|
|
84
|
+
/**
|
|
85
|
+
* Whether a proxy url passes validation: a blank url is the "disable proxy"
|
|
86
|
+
* marker and passes; anything else must parse as an http(s) URL. This is a
|
|
87
|
+
* pure syntax check — no connectivity check is performed.
|
|
88
|
+
*/
|
|
89
|
+
export declare function isValidProxyUrl(url: string): boolean;
|
|
90
|
+
export declare function extractProxyConfig(value: unknown): ProxyConfig | undefined;
|
|
64
91
|
export declare function usableSearchProviders(search: SearchBackendConfig, env?: NodeJS.ProcessEnv): SearchProviderName[];
|
|
65
92
|
export declare function extractBackendConfigOverride(file: BackendConfigFile | null | undefined): BackendConfigOverride;
|
|
66
93
|
export declare function validateBackendConfig(config: BackendConfig): string[];
|
package/dist/backends/config.js
CHANGED
|
@@ -9,6 +9,76 @@ function extractStringArray(value) {
|
|
|
9
9
|
const strings = value.filter((item) => typeof item === 'string');
|
|
10
10
|
return strings.length === value.length ? strings : undefined;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Remove any credentials (user:pass@) embedded in a proxy URL. pi-web-agent
|
|
14
|
+
* never reads or sends credentials from the URL; proxy auth comes from
|
|
15
|
+
* backends.proxy.username/password or PI_WEB_AGENT_PROXY_USERNAME /
|
|
16
|
+
* PI_WEB_AGENT_PROXY_PASSWORD. Stripping them here keeps them out of logs,
|
|
17
|
+
* doctor output, the settings UI, and the proxy connection itself.
|
|
18
|
+
*/
|
|
19
|
+
export function stripProxyCredentials(url) {
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = new URL(url);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return url;
|
|
26
|
+
}
|
|
27
|
+
if (!parsed.username && !parsed.password)
|
|
28
|
+
return url; // already credential-free
|
|
29
|
+
parsed.username = '';
|
|
30
|
+
parsed.password = '';
|
|
31
|
+
return parsed.toString().replace(/\/$/, '');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Whether a proxy url passes validation: a blank url is the "disable proxy"
|
|
35
|
+
* marker and passes; anything else must parse as an http(s) URL. This is a
|
|
36
|
+
* pure syntax check — no connectivity check is performed.
|
|
37
|
+
*/
|
|
38
|
+
export function isValidProxyUrl(url) {
|
|
39
|
+
if (url.trim() === '')
|
|
40
|
+
return true;
|
|
41
|
+
try {
|
|
42
|
+
const parsed = new URL(url);
|
|
43
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function extractProxyConfig(value) {
|
|
50
|
+
if (!value || typeof value !== 'object')
|
|
51
|
+
return undefined;
|
|
52
|
+
const raw = value;
|
|
53
|
+
// An explicitly present but blank url is the "disable proxy" marker: it lets a
|
|
54
|
+
// higher-priority layer (e.g. a project) clear a proxy set in a lower layer.
|
|
55
|
+
if (typeof raw.url === 'string' && raw.url.trim() === '') {
|
|
56
|
+
return { url: '' };
|
|
57
|
+
}
|
|
58
|
+
if (typeof raw.url !== 'string' || !raw.url.trim())
|
|
59
|
+
return undefined;
|
|
60
|
+
const url = raw.url.trim();
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = new URL(url);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
parsed = undefined;
|
|
67
|
+
}
|
|
68
|
+
// A valid url is normalized. A malformed url is kept as-is (rather than
|
|
69
|
+
// dropped) so validation can flag it and the backend factory can fail loudly
|
|
70
|
+
// instead of silently sending traffic direct to the websites.
|
|
71
|
+
const config = {
|
|
72
|
+
url: parsed && (parsed.protocol === 'http:' || parsed.protocol === 'https:')
|
|
73
|
+
? parsed.toString().replace(/\/$/, '')
|
|
74
|
+
: url
|
|
75
|
+
};
|
|
76
|
+
if (typeof raw.username === 'string' && raw.username.trim())
|
|
77
|
+
config.username = raw.username;
|
|
78
|
+
if (typeof raw.password === 'string')
|
|
79
|
+
config.password = raw.password;
|
|
80
|
+
return config;
|
|
81
|
+
}
|
|
12
82
|
function extractSearxngOptions(value) {
|
|
13
83
|
if (!value || typeof value !== 'object')
|
|
14
84
|
return undefined;
|
|
@@ -113,6 +183,10 @@ export function extractBackendConfigOverride(file) {
|
|
|
113
183
|
if (backends?.headless?.provider === 'local-browser') {
|
|
114
184
|
override.headless = { provider: 'local-browser' };
|
|
115
185
|
}
|
|
186
|
+
const proxy = extractProxyConfig(backends?.proxy);
|
|
187
|
+
if (proxy) {
|
|
188
|
+
override.proxy = proxy;
|
|
189
|
+
}
|
|
116
190
|
return override;
|
|
117
191
|
}
|
|
118
192
|
export function validateBackendConfig(config) {
|
|
@@ -120,6 +194,23 @@ export function validateBackendConfig(config) {
|
|
|
120
194
|
if (config.search.provider === 'searxng' && !config.search.baseUrl) {
|
|
121
195
|
issues.push('search provider searxng requires backends.search.baseUrl');
|
|
122
196
|
}
|
|
197
|
+
if (config.proxy && config.proxy.url.trim() !== '') {
|
|
198
|
+
let parsed;
|
|
199
|
+
try {
|
|
200
|
+
parsed = new URL(config.proxy.url);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
parsed = undefined;
|
|
204
|
+
}
|
|
205
|
+
if (!isValidProxyUrl(config.proxy.url)) {
|
|
206
|
+
issues.push('backends.proxy.url must be an http or https URL');
|
|
207
|
+
}
|
|
208
|
+
else if (parsed && (parsed.username || parsed.password)) {
|
|
209
|
+
// Credentials belong in backends.proxy.username/password or the env vars
|
|
210
|
+
// below, never in the URL itself.
|
|
211
|
+
issues.push('backends.proxy.url must not include credentials (user:pass@); set PI_WEB_AGENT_PROXY_USERNAME and PI_WEB_AGENT_PROXY_PASSWORD (or backends.proxy.username / backends.proxy.password) instead');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
123
214
|
if (config.fetch.provider === 'firecrawl' && !config.fetch.baseUrl) {
|
|
124
215
|
issues.push('fetch provider firecrawl requires backends.fetch.baseUrl');
|
|
125
216
|
}
|
|
@@ -173,6 +264,11 @@ export function mergeBackendConfigLayers(...layers) {
|
|
|
173
264
|
return layers.reduce((merged, layer) => ({
|
|
174
265
|
search: mergeSearchConfig(merged.search, layer?.search),
|
|
175
266
|
fetch: mergeFetchConfig(merged.fetch, layer?.fetch),
|
|
176
|
-
headless: { ...merged.headless, ...layer?.headless }
|
|
267
|
+
headless: { ...merged.headless, ...layer?.headless },
|
|
268
|
+
proxy: layer?.proxy
|
|
269
|
+
? layer.proxy.url === ''
|
|
270
|
+
? undefined // explicit disable overrides any proxy from lower layers
|
|
271
|
+
: { ...merged.proxy, ...layer.proxy }
|
|
272
|
+
: merged.proxy
|
|
177
273
|
}), DEFAULT_BACKEND_CONFIG);
|
|
178
274
|
}
|
|
@@ -8,7 +8,7 @@ import { createWebFetchHeadlessTool } from '../tools/web-fetch-headless.js';
|
|
|
8
8
|
import { createWebFetchTool } from '../tools/web-fetch.js';
|
|
9
9
|
import { createWebSearchTool } from '../tools/web-search.js';
|
|
10
10
|
import type { WebFetchHeadlessResponse, WebFetchResponse, WebSearchResponse } from '../types.js';
|
|
11
|
-
import { type BackendConfig } from './config.js';
|
|
11
|
+
import { type BackendConfig, type ProxyConfig } from './config.js';
|
|
12
12
|
export type BackendSet = {
|
|
13
13
|
search: (input: {
|
|
14
14
|
query: string;
|
|
@@ -30,5 +30,6 @@ export type BackendFactoryDeps = {
|
|
|
30
30
|
createHttpFetch?: typeof createWebFetchTool;
|
|
31
31
|
createFirecrawlFetch?: typeof createFirecrawlFetcher;
|
|
32
32
|
createHeadlessFetch?: typeof createWebFetchHeadlessTool;
|
|
33
|
+
createProxyFetch?: (proxy: ProxyConfig) => typeof fetch;
|
|
33
34
|
};
|
|
34
35
|
export declare function createBackendSet(config?: BackendConfig, deps?: BackendFactoryDeps): BackendSet;
|