@demigodmode/pi-web-agent 0.5.1 → 1.0.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 +192 -0
- package/README.md +7 -0
- package/dist/backends/config.d.ts +42 -0
- package/dist/backends/config.js +61 -0
- package/dist/backends/doctor.d.ts +5 -0
- package/dist/backends/doctor.js +68 -0
- package/dist/backends/factory.d.ts +14 -0
- package/dist/backends/factory.js +58 -0
- package/dist/changelog-notice.d.ts +16 -0
- package/dist/changelog-notice.js +105 -0
- package/dist/commands/web-agent-config.d.ts +4 -1
- package/dist/commands/web-agent-config.js +35 -5
- package/dist/extension.d.ts +1 -1
- package/dist/extension.js +45 -4
- package/dist/fetch/firecrawl-fetch.d.ts +6 -0
- package/dist/fetch/firecrawl-fetch.js +61 -0
- package/dist/orchestration/index.d.ts +3 -1
- package/dist/orchestration/index.js +8 -6
- package/dist/orchestration/research-types.d.ts +1 -1
- package/dist/presentation/config-store.d.ts +3 -0
- package/dist/presentation/config-store.js +11 -2
- package/dist/presentation/explore-presentation.js +2 -0
- package/dist/search/searxng.d.ts +7 -0
- package/dist/search/searxng.js +66 -0
- package/dist/tools/web-explore.d.ts +1 -1
- package/dist/types.d.ts +3 -3
- package/package.json +7 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is intentionally simple and release-oriented.
|
|
6
|
+
|
|
7
|
+
## Unreleased
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Nothing yet.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Nothing yet.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Nothing yet.
|
|
17
|
+
|
|
18
|
+
### Breaking
|
|
19
|
+
- None.
|
|
20
|
+
|
|
21
|
+
## [1.0.0] - 2026-05-08
|
|
22
|
+
### Added
|
|
23
|
+
- Added one-time `pi-web-agent` changelog notices after package updates and `/web-agent changelog` for manual viewing.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Migrated Pi package imports to `@earendil-works/*` after the upstream Pi scope move.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- Nothing yet.
|
|
30
|
+
|
|
31
|
+
### Breaking
|
|
32
|
+
- 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.
|
|
33
|
+
|
|
34
|
+
## [0.6.0] - 2026-05-04
|
|
35
|
+
### Added
|
|
36
|
+
- Added configurable web backends for `web_explore`, including SearXNG search and Firecrawl fetch support.
|
|
37
|
+
- Added backend diagnostics to `/web-agent doctor`, including config validation and self-hosted endpoint checks.
|
|
38
|
+
- Added dedicated self-hosted backend docs for connecting existing SearXNG and Firecrawl services.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
- `/web-agent show` now includes the effective backend configuration.
|
|
42
|
+
- `web_explore` now loads the effective backend config while preserving the default DuckDuckGo, HTTP, and local-browser behavior.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
- Fixed backend config merging so provider-specific fields do not leak when a higher-precedence config changes providers.
|
|
46
|
+
- Kept the configured `web_explore` workflow reusable while backend config is unchanged, avoiding unnecessary backend/cache recreation.
|
|
47
|
+
|
|
48
|
+
### Breaking
|
|
49
|
+
- None.
|
|
50
|
+
|
|
51
|
+
## [0.5.1] - 2026-05-04
|
|
52
|
+
### Added
|
|
53
|
+
- Nothing yet.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
- Nothing yet.
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
- Fixed the Windows browser-resolution test so it is deterministic on Linux CI.
|
|
60
|
+
|
|
61
|
+
### Breaking
|
|
62
|
+
- None.
|
|
63
|
+
|
|
64
|
+
## [0.5.0] - 2026-05-04
|
|
65
|
+
### Added
|
|
66
|
+
- Added `/web-agent doctor` to report extension, runtime dependency, and browser detection status.
|
|
67
|
+
- Added a `/web-agent` action menu for settings, config display, doctor, and reset actions.
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
- Migrated runtime schema imports from `@sinclair/typebox` to `typebox` for Pi 0.69 compatibility.
|
|
71
|
+
- Documented the current headless rendering browser requirement and doctor command.
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
- Fixed headless browser detection so Chrome, Chromium, Edge, and Brave can be found across Windows, macOS, and Linux instead of only checking Windows Chrome/Edge paths.
|
|
75
|
+
|
|
76
|
+
### Breaking
|
|
77
|
+
- None.
|
|
78
|
+
|
|
79
|
+
## [0.4.0] - 2026-04-29
|
|
80
|
+
### Added
|
|
81
|
+
- Made `web_explore` the single public web research tool, with search, fetch, source ranking, and headless escalation handled internally.
|
|
82
|
+
- Added adaptive research helpers for query planning, candidate selection, evidence ranking, stop decisions, and answer synthesis.
|
|
83
|
+
- Added preview/verbose provenance for `web_explore` showing which internal reader produced each finding.
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
- Simplified `/web-agent` presentation settings to `defaultMode` and `web_explore` only.
|
|
87
|
+
- Updated live web evals to treat shell/network fallbacks after `web_explore` as a quality issue.
|
|
88
|
+
- Updated the release script to use `npm version --no-git-tag-version` before tagging so package metadata is changed through npm instead of regex replacement.
|
|
89
|
+
|
|
90
|
+
### Fixed
|
|
91
|
+
- Turned successful headless reads into usable `web_explore` evidence instead of returning empty results for dynamic docs pages.
|
|
92
|
+
- Filtered headless bot-check/security-verification pages out of research evidence.
|
|
93
|
+
- Made empty research results display as “No usable evidence found” instead of looking like a successful synthesis.
|
|
94
|
+
- Added the Linux Rollup optional package to the lockfile so GitHub Actions can build from `npm ci` without patch-installing Rollup.
|
|
95
|
+
|
|
96
|
+
### Breaking
|
|
97
|
+
- None.
|
|
98
|
+
|
|
99
|
+
## [0.3.1] - 2026-04-22
|
|
100
|
+
### Added
|
|
101
|
+
- Nothing yet.
|
|
102
|
+
|
|
103
|
+
### Changed
|
|
104
|
+
- Stopped self-upgrading npm inside the publish workflow before install and publish steps.
|
|
105
|
+
- Added GitHub release creation to the tag publish workflow.
|
|
106
|
+
|
|
107
|
+
### Fixed
|
|
108
|
+
- Fixed the tag publish workflow so npm publishing no longer fails before `npm ci`.
|
|
109
|
+
|
|
110
|
+
### Breaking
|
|
111
|
+
- None.
|
|
112
|
+
|
|
113
|
+
## [0.3.0] - 2026-04-22
|
|
114
|
+
### Added
|
|
115
|
+
- Added compact, preview, and verbose presentation modes for web tool output.
|
|
116
|
+
- Added a user-facing `/web-agent` settings UI plus helper commands for showing, resetting, and changing presentation config.
|
|
117
|
+
- Added global and project-local presentation config files with project-overrides-global precedence.
|
|
118
|
+
- Added docs for presentation settings, config paths, and command usage.
|
|
119
|
+
|
|
120
|
+
### Changed
|
|
121
|
+
- Made compact output the default presentation mode for all web tools.
|
|
122
|
+
- Made bare `/web-agent` open the settings UI directly.
|
|
123
|
+
|
|
124
|
+
### Fixed
|
|
125
|
+
- Fixed settings scope switching so global and project drafts do not leak into each other.
|
|
126
|
+
- Fixed config persistence so inherited values are not unnecessarily written into lower-precedence config files.
|
|
127
|
+
- Fixed command notifications to use supported Pi UI notify levels.
|
|
128
|
+
|
|
129
|
+
### Breaking
|
|
130
|
+
- None.
|
|
131
|
+
|
|
132
|
+
## [0.2.2] - 2026-04-21
|
|
133
|
+
### Added
|
|
134
|
+
- Expanded the live web eval so it covers deterministic search failure cases and reports when follow-up web calls were blocked after `web_explore`.
|
|
135
|
+
|
|
136
|
+
### Changed
|
|
137
|
+
- Tightened post-`web_explore` discipline by blocking same-flow low-level web tool churn instead of relying on prompt wording alone.
|
|
138
|
+
|
|
139
|
+
### Fixed
|
|
140
|
+
- Split `web_search` failures into more useful states like no results, parse failures, blocked pages, and fetch failures.
|
|
141
|
+
- Catch DuckDuckGo challenge pages that still return HTTP 200 so blocked searches stop looking like vague parser bugs.
|
|
142
|
+
- Stopped the model from spiraling into extra `web_search` / `web_fetch` calls after a successful `web_explore` in the live-eval cases.
|
|
143
|
+
|
|
144
|
+
### Breaking
|
|
145
|
+
- None.
|
|
146
|
+
|
|
147
|
+
## [0.2.1] - 2026-04-20
|
|
148
|
+
### Added
|
|
149
|
+
- Nothing yet.
|
|
150
|
+
|
|
151
|
+
### Changed
|
|
152
|
+
- Nothing yet.
|
|
153
|
+
|
|
154
|
+
### Fixed
|
|
155
|
+
- Added the missing npm `--provenance` flag to the publish workflow so Trusted Publishing can exchange the GitHub OIDC token correctly.
|
|
156
|
+
|
|
157
|
+
### Breaking
|
|
158
|
+
- None.
|
|
159
|
+
|
|
160
|
+
## [0.2.0] - 2026-04-20
|
|
161
|
+
### Added
|
|
162
|
+
- Added AGPL licensing, a release foundation test, and changelog-driven release tooling.
|
|
163
|
+
- Added GitHub Actions workflows for CI and tag-based npm publishing.
|
|
164
|
+
- Added maintainer docs for releases, self-hosted runners, and main branch protection.
|
|
165
|
+
|
|
166
|
+
### Changed
|
|
167
|
+
- Documented the release process in the README.
|
|
168
|
+
- Switched npm publishing guidance from `NPM_TOKEN` secrets to npm Trusted Publishing.
|
|
169
|
+
|
|
170
|
+
### Fixed
|
|
171
|
+
- Stopped injecting post-`web_explore` reminder text through a context hook so it no longer leaks into normal sessions.
|
|
172
|
+
- Worked around Rollup's missing Linux native package in GitHub Actions so CI and publish jobs run reliably on Ubuntu.
|
|
173
|
+
|
|
174
|
+
### Breaking
|
|
175
|
+
- None.
|
|
176
|
+
|
|
177
|
+
## [0.1.0] - 2026-04-20
|
|
178
|
+
|
|
179
|
+
### Added
|
|
180
|
+
- Published `@demigodmode/pi-web-agent` as a Pi package.
|
|
181
|
+
- Added explicit web research tools for search, HTTP fetch, headless fetch, and bounded exploration.
|
|
182
|
+
- Added headless fetch implementation and package install validation.
|
|
183
|
+
|
|
184
|
+
### Changed
|
|
185
|
+
- Tightened follow-up tool discipline after `web_explore`.
|
|
186
|
+
- Split package build output from repo-local development tooling.
|
|
187
|
+
|
|
188
|
+
### Fixed
|
|
189
|
+
- Fixed post-`web_explore` reminder handling so it is derived from context instead of shared mutable state.
|
|
190
|
+
|
|
191
|
+
### Breaking
|
|
192
|
+
- None.
|
package/README.md
CHANGED
|
@@ -14,6 +14,8 @@ That sounds obvious, but a lot of agent tooling gets fuzzy right there. This pac
|
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
17
|
+
Compatibility notice: current `pi-web-agent` requires Pi 0.74+ because Pi packages moved to the `@earendil-works/*` scope. Update Pi before updating this package. If you are on an older Pi version, stay on `@demigodmode/pi-web-agent@0.6.x` until Pi is updated.
|
|
18
|
+
|
|
17
19
|
```bash
|
|
18
20
|
pi install npm:@demigodmode/pi-web-agent
|
|
19
21
|
```
|
|
@@ -71,6 +73,7 @@ Helper commands:
|
|
|
71
73
|
```text
|
|
72
74
|
/web-agent doctor
|
|
73
75
|
/web-agent show
|
|
76
|
+
/web-agent changelog
|
|
74
77
|
/web-agent reset project
|
|
75
78
|
/web-agent reset global
|
|
76
79
|
/web-agent mode preview
|
|
@@ -106,6 +109,10 @@ Example:
|
|
|
106
109
|
}
|
|
107
110
|
```
|
|
108
111
|
|
|
112
|
+
Backend config is also supported. Defaults remain DuckDuckGo search, plain HTTP fetch, and local browser headless fallback. If you already run SearXNG or Firecrawl, see the self-hosted backend guide:
|
|
113
|
+
|
|
114
|
+
- https://demigodmode.github.io/pi-web-agent/self-hosted-backends
|
|
115
|
+
|
|
109
116
|
## Local development
|
|
110
117
|
|
|
111
118
|
```bash
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type SearchBackendConfig = {
|
|
2
|
+
provider: 'duckduckgo' | 'searxng';
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
};
|
|
5
|
+
export type FetchBackendConfig = {
|
|
6
|
+
provider: 'http' | 'firecrawl';
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
};
|
|
10
|
+
export type HeadlessBackendConfig = {
|
|
11
|
+
provider: 'local-browser';
|
|
12
|
+
};
|
|
13
|
+
export type BackendConfig = {
|
|
14
|
+
search: SearchBackendConfig;
|
|
15
|
+
fetch: FetchBackendConfig;
|
|
16
|
+
headless: HeadlessBackendConfig;
|
|
17
|
+
};
|
|
18
|
+
export type BackendConfigOverride = {
|
|
19
|
+
search?: Partial<SearchBackendConfig>;
|
|
20
|
+
fetch?: Partial<FetchBackendConfig>;
|
|
21
|
+
headless?: Partial<HeadlessBackendConfig>;
|
|
22
|
+
};
|
|
23
|
+
export type BackendConfigFile = {
|
|
24
|
+
backends?: {
|
|
25
|
+
search?: {
|
|
26
|
+
provider?: unknown;
|
|
27
|
+
baseUrl?: unknown;
|
|
28
|
+
};
|
|
29
|
+
fetch?: {
|
|
30
|
+
provider?: unknown;
|
|
31
|
+
baseUrl?: unknown;
|
|
32
|
+
apiKey?: unknown;
|
|
33
|
+
};
|
|
34
|
+
headless?: {
|
|
35
|
+
provider?: unknown;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
export declare const DEFAULT_BACKEND_CONFIG: BackendConfig;
|
|
40
|
+
export declare function extractBackendConfigOverride(file: BackendConfigFile | null | undefined): BackendConfigOverride;
|
|
41
|
+
export declare function validateBackendConfig(config: BackendConfig): string[];
|
|
42
|
+
export declare function mergeBackendConfigLayers(...layers: Array<BackendConfig | BackendConfigOverride | undefined>): BackendConfig;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export const DEFAULT_BACKEND_CONFIG = {
|
|
2
|
+
search: { provider: 'duckduckgo' },
|
|
3
|
+
fetch: { provider: 'http' },
|
|
4
|
+
headless: { provider: 'local-browser' }
|
|
5
|
+
};
|
|
6
|
+
export function extractBackendConfigOverride(file) {
|
|
7
|
+
const backends = file?.backends;
|
|
8
|
+
const override = {};
|
|
9
|
+
if (backends?.search?.provider === 'duckduckgo' || backends?.search?.provider === 'searxng') {
|
|
10
|
+
override.search = { provider: backends.search.provider };
|
|
11
|
+
if (typeof backends.search.baseUrl === 'string') {
|
|
12
|
+
override.search.baseUrl = backends.search.baseUrl;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (backends?.fetch?.provider === 'http' || backends?.fetch?.provider === 'firecrawl') {
|
|
16
|
+
override.fetch = { provider: backends.fetch.provider };
|
|
17
|
+
if (typeof backends.fetch.baseUrl === 'string') {
|
|
18
|
+
override.fetch.baseUrl = backends.fetch.baseUrl;
|
|
19
|
+
}
|
|
20
|
+
if (typeof backends.fetch.apiKey === 'string') {
|
|
21
|
+
override.fetch.apiKey = backends.fetch.apiKey;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (backends?.headless?.provider === 'local-browser') {
|
|
25
|
+
override.headless = { provider: 'local-browser' };
|
|
26
|
+
}
|
|
27
|
+
return override;
|
|
28
|
+
}
|
|
29
|
+
export function validateBackendConfig(config) {
|
|
30
|
+
const issues = [];
|
|
31
|
+
if (config.search.provider === 'searxng' && !config.search.baseUrl) {
|
|
32
|
+
issues.push('search provider searxng requires backends.search.baseUrl');
|
|
33
|
+
}
|
|
34
|
+
if (config.fetch.provider === 'firecrawl' && !config.fetch.baseUrl) {
|
|
35
|
+
issues.push('fetch provider firecrawl requires backends.fetch.baseUrl');
|
|
36
|
+
}
|
|
37
|
+
return issues;
|
|
38
|
+
}
|
|
39
|
+
function mergeSearchConfig(current, override) {
|
|
40
|
+
if (!override)
|
|
41
|
+
return current;
|
|
42
|
+
if (override.provider && override.provider !== current.provider) {
|
|
43
|
+
return { ...override, provider: override.provider };
|
|
44
|
+
}
|
|
45
|
+
return { ...current, ...override };
|
|
46
|
+
}
|
|
47
|
+
function mergeFetchConfig(current, override) {
|
|
48
|
+
if (!override)
|
|
49
|
+
return current;
|
|
50
|
+
if (override.provider && override.provider !== current.provider) {
|
|
51
|
+
return { ...override, provider: override.provider };
|
|
52
|
+
}
|
|
53
|
+
return { ...current, ...override };
|
|
54
|
+
}
|
|
55
|
+
export function mergeBackendConfigLayers(...layers) {
|
|
56
|
+
return layers.reduce((merged, layer) => ({
|
|
57
|
+
search: mergeSearchConfig(merged.search, layer?.search),
|
|
58
|
+
fetch: mergeFetchConfig(merged.fetch, layer?.fetch),
|
|
59
|
+
headless: { ...merged.headless, ...layer?.headless }
|
|
60
|
+
}), DEFAULT_BACKEND_CONFIG);
|
|
61
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
function withTimeout(timeoutMs) {
|
|
2
|
+
const controller = new AbortController();
|
|
3
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
4
|
+
return { signal: controller.signal, done: () => clearTimeout(timeout) };
|
|
5
|
+
}
|
|
6
|
+
function message(error) {
|
|
7
|
+
return error instanceof Error ? error.message : String(error);
|
|
8
|
+
}
|
|
9
|
+
function searxngDoctorUrl(baseUrl) {
|
|
10
|
+
const url = new URL('/search', baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);
|
|
11
|
+
url.searchParams.set('q', 'pi-web-agent-doctor');
|
|
12
|
+
url.searchParams.set('format', 'json');
|
|
13
|
+
return url.toString();
|
|
14
|
+
}
|
|
15
|
+
export async function checkBackendHealth(config, { fetchImpl = fetch, timeoutMs = 3_000 } = {}) {
|
|
16
|
+
const lines = [];
|
|
17
|
+
if (config.search.provider === 'duckduckgo') {
|
|
18
|
+
lines.push('search backend: duckduckgo');
|
|
19
|
+
}
|
|
20
|
+
else if (!config.search.baseUrl) {
|
|
21
|
+
lines.push('search backend: searxng warning (missing baseUrl)');
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
const timeout = withTimeout(timeoutMs);
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetchImpl(searxngDoctorUrl(config.search.baseUrl), { signal: timeout.signal });
|
|
27
|
+
const json = (await response.json());
|
|
28
|
+
lines.push(response.ok && Array.isArray(json.results)
|
|
29
|
+
? 'search backend: searxng ok'
|
|
30
|
+
: 'search backend: searxng warning (unexpected response)');
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
lines.push(`search backend: searxng warning (${message(error)})`);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
timeout.done();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (config.fetch.provider === 'http') {
|
|
40
|
+
lines.push('fetch backend: http');
|
|
41
|
+
}
|
|
42
|
+
else if (!config.fetch.baseUrl) {
|
|
43
|
+
lines.push('fetch backend: firecrawl warning (missing baseUrl)');
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const timeout = withTimeout(timeoutMs);
|
|
47
|
+
try {
|
|
48
|
+
const headers = { 'content-type': 'application/json' };
|
|
49
|
+
if (config.fetch.apiKey ?? process.env.PI_WEB_AGENT_FIRECRAWL_API_KEY) {
|
|
50
|
+
headers.Authorization = `Bearer ${config.fetch.apiKey ?? process.env.PI_WEB_AGENT_FIRECRAWL_API_KEY}`;
|
|
51
|
+
}
|
|
52
|
+
const response = await fetchImpl(new URL('/v1/scrape', config.fetch.baseUrl).toString(), {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers,
|
|
55
|
+
body: JSON.stringify({ url: 'https://example.com', formats: ['markdown'] }),
|
|
56
|
+
signal: timeout.signal
|
|
57
|
+
});
|
|
58
|
+
lines.push(response.ok ? 'fetch backend: firecrawl ok' : `fetch backend: firecrawl warning (HTTP ${response.status})`);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
lines.push(`fetch backend: firecrawl warning (${message(error)})`);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
timeout.done();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return lines;
|
|
68
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { WebFetchHeadlessResponse, WebFetchResponse, WebSearchResponse } from '../types.js';
|
|
2
|
+
import { type BackendConfig } from './config.js';
|
|
3
|
+
export type BackendSet = {
|
|
4
|
+
search: (input: {
|
|
5
|
+
query: string;
|
|
6
|
+
}) => Promise<WebSearchResponse>;
|
|
7
|
+
fetchPage: (input: {
|
|
8
|
+
url: string;
|
|
9
|
+
}) => Promise<WebFetchResponse>;
|
|
10
|
+
headlessFetch: (input: {
|
|
11
|
+
url: string;
|
|
12
|
+
}) => Promise<WebFetchHeadlessResponse>;
|
|
13
|
+
};
|
|
14
|
+
export declare function createBackendSet(config?: BackendConfig): BackendSet;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createFirecrawlFetcher } from '../fetch/firecrawl-fetch.js';
|
|
2
|
+
import { createSearxngSearchTool } from '../search/searxng.js';
|
|
3
|
+
import { buildFetchPresentation } from '../presentation/fetch-presentation.js';
|
|
4
|
+
import { buildSearchPresentation } from '../presentation/search-presentation.js';
|
|
5
|
+
import { createWebFetchHeadlessTool } from '../tools/web-fetch-headless.js';
|
|
6
|
+
import { createWebFetchTool } from '../tools/web-fetch.js';
|
|
7
|
+
import { createWebSearchTool } from '../tools/web-search.js';
|
|
8
|
+
import { DEFAULT_BACKEND_CONFIG } from './config.js';
|
|
9
|
+
function invalidSearxngSearch() {
|
|
10
|
+
return async function search() {
|
|
11
|
+
const result = {
|
|
12
|
+
status: 'error',
|
|
13
|
+
results: [],
|
|
14
|
+
metadata: { backend: 'searxng', cacheHit: false },
|
|
15
|
+
error: {
|
|
16
|
+
code: 'BACKEND_CONFIG_INVALID',
|
|
17
|
+
message: 'SearXNG search requires backends.search.baseUrl.'
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
return { ...result, presentation: buildSearchPresentation(result) };
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function invalidFirecrawlFetch() {
|
|
24
|
+
return async function fetchPage(url) {
|
|
25
|
+
const result = {
|
|
26
|
+
status: 'error',
|
|
27
|
+
url,
|
|
28
|
+
metadata: { method: 'firecrawl', cacheHit: false },
|
|
29
|
+
error: {
|
|
30
|
+
code: 'BACKEND_CONFIG_INVALID',
|
|
31
|
+
message: 'Firecrawl fetch requires backends.fetch.baseUrl.'
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
return { ...result, presentation: buildFetchPresentation(result) };
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function createBackendSet(config = DEFAULT_BACKEND_CONFIG) {
|
|
38
|
+
const search = config.search.provider === 'searxng'
|
|
39
|
+
? config.search.baseUrl
|
|
40
|
+
? createSearxngSearchTool({ baseUrl: config.search.baseUrl })
|
|
41
|
+
: invalidSearxngSearch()
|
|
42
|
+
: createWebSearchTool();
|
|
43
|
+
const fetchPage = config.fetch.provider === 'firecrawl'
|
|
44
|
+
? config.fetch.baseUrl
|
|
45
|
+
? createWebFetchTool({
|
|
46
|
+
fetchPage: createFirecrawlFetcher({
|
|
47
|
+
baseUrl: config.fetch.baseUrl,
|
|
48
|
+
apiKey: config.fetch.apiKey ?? process.env.PI_WEB_AGENT_FIRECRAWL_API_KEY
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
: createWebFetchTool({ fetchPage: invalidFirecrawlFetch() })
|
|
52
|
+
: createWebFetchTool();
|
|
53
|
+
return {
|
|
54
|
+
search,
|
|
55
|
+
fetchPage,
|
|
56
|
+
headlessFetch: createWebFetchHeadlessTool()
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type ChangelogEntry = {
|
|
2
|
+
version: string;
|
|
3
|
+
content: string;
|
|
4
|
+
};
|
|
5
|
+
type ChangelogOptions = {
|
|
6
|
+
packageRoot?: string;
|
|
7
|
+
statePath?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function parseChangelogEntries(changelog: string): ChangelogEntry[];
|
|
10
|
+
export declare function markChangelogSeen({ statePath, version }: {
|
|
11
|
+
statePath?: string;
|
|
12
|
+
version: string;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
export declare function getUpdateChangelogNotice(options?: ChangelogOptions): Promise<string | undefined>;
|
|
15
|
+
export declare function getLatestChangelogEntry(options?: ChangelogOptions): Promise<string | undefined>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
export function parseChangelogEntries(changelog) {
|
|
5
|
+
const lines = changelog.split(/\r?\n/);
|
|
6
|
+
const entries = [];
|
|
7
|
+
let currentVersion;
|
|
8
|
+
let currentLines = [];
|
|
9
|
+
function flush() {
|
|
10
|
+
if (currentVersion && currentLines.length > 0) {
|
|
11
|
+
entries.push({ version: currentVersion, content: currentLines.join('\n').trim() });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
if (line.startsWith('## ')) {
|
|
16
|
+
flush();
|
|
17
|
+
const match = line.match(/^##\s+\[?(\d+\.\d+\.\d+)\]?/);
|
|
18
|
+
currentVersion = match?.[1];
|
|
19
|
+
currentLines = currentVersion ? [line] : [];
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (currentVersion) {
|
|
23
|
+
currentLines.push(line);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
flush();
|
|
27
|
+
return entries;
|
|
28
|
+
}
|
|
29
|
+
function compareVersions(left, right) {
|
|
30
|
+
const leftParts = left.split('.').map(Number);
|
|
31
|
+
const rightParts = right.split('.').map(Number);
|
|
32
|
+
for (let i = 0; i < 3; i += 1) {
|
|
33
|
+
const diff = (leftParts[i] || 0) - (rightParts[i] || 0);
|
|
34
|
+
if (diff !== 0)
|
|
35
|
+
return diff;
|
|
36
|
+
}
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
function defaultPackageRoot() {
|
|
40
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
return path.basename(here) === 'dist' ? path.dirname(here) : path.dirname(here);
|
|
42
|
+
}
|
|
43
|
+
function defaultStatePath() {
|
|
44
|
+
const homeDir = process.env.USERPROFILE ?? process.env.HOME ?? '';
|
|
45
|
+
return path.join(homeDir, '.pi', 'agent', 'extensions', 'pi-web-agent', 'state.json');
|
|
46
|
+
}
|
|
47
|
+
async function readPackageVersion(packageRoot) {
|
|
48
|
+
try {
|
|
49
|
+
const raw = await readFile(path.join(packageRoot, 'package.json'), 'utf8');
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
return typeof parsed.version === 'string' ? parsed.version : undefined;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function readChangelogEntries(packageRoot) {
|
|
58
|
+
try {
|
|
59
|
+
return parseChangelogEntries(await readFile(path.join(packageRoot, 'CHANGELOG.md'), 'utf8'));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function readState(statePath) {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(await readFile(statePath, 'utf8'));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export async function markChangelogSeen({ statePath = defaultStatePath(), version }) {
|
|
74
|
+
try {
|
|
75
|
+
await mkdir(path.dirname(statePath), { recursive: true });
|
|
76
|
+
await writeFile(statePath, `${JSON.stringify({ lastChangelogVersion: version }, null, 2)}\n`, 'utf8');
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Changelog display is best effort. Never block extension startup on state persistence.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export async function getUpdateChangelogNotice(options = {}) {
|
|
83
|
+
const packageRoot = options.packageRoot ?? defaultPackageRoot();
|
|
84
|
+
const statePath = options.statePath ?? defaultStatePath();
|
|
85
|
+
const version = await readPackageVersion(packageRoot);
|
|
86
|
+
if (!version)
|
|
87
|
+
return undefined;
|
|
88
|
+
const state = await readState(statePath);
|
|
89
|
+
if (!state.lastChangelogVersion) {
|
|
90
|
+
await markChangelogSeen({ statePath, version });
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
if (compareVersions(version, state.lastChangelogVersion) <= 0) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const entries = (await readChangelogEntries(packageRoot))
|
|
97
|
+
.filter((entry) => compareVersions(entry.version, state.lastChangelogVersion || '0.0.0') > 0)
|
|
98
|
+
.map((entry) => entry.content);
|
|
99
|
+
await markChangelogSeen({ statePath, version });
|
|
100
|
+
return entries.length > 0 ? entries.join('\n\n') : undefined;
|
|
101
|
+
}
|
|
102
|
+
export async function getLatestChangelogEntry(options = {}) {
|
|
103
|
+
const entries = await readChangelogEntries(options.packageRoot ?? defaultPackageRoot());
|
|
104
|
+
return entries[0]?.content;
|
|
105
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type BackendConfig } from '../backends/config.js';
|
|
2
|
+
import { type ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
3
|
import { loadPresentationConfigLayers, type LoadedPresentationConfig } from '../presentation/config-store.js';
|
|
3
4
|
import { type BrowserResolutionResult } from '../fetch/browser-resolution.js';
|
|
4
5
|
import type { PresentationConfig, PresentationConfigOverride, PresentationScope } from '../presentation/types.js';
|
|
@@ -13,6 +14,8 @@ type CommandDeps = {
|
|
|
13
14
|
arch: string;
|
|
14
15
|
};
|
|
15
16
|
checkTypebox?: () => Promise<boolean>;
|
|
17
|
+
checkBackends?: (config: BackendConfig) => Promise<string[]>;
|
|
18
|
+
getChangelog?: () => Promise<string | undefined>;
|
|
16
19
|
};
|
|
17
20
|
export type SettingsDraftState = {
|
|
18
21
|
scope: PresentationScope;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { DEFAULT_BACKEND_CONFIG, validateBackendConfig } from '../backends/config.js';
|
|
2
|
+
import { checkBackendHealth } from '../backends/doctor.js';
|
|
3
|
+
import { DynamicBorder, getSettingsListTheme } from '@earendil-works/pi-coding-agent';
|
|
4
|
+
import { Container, SelectList, SettingsList, Text } from '@earendil-works/pi-tui';
|
|
3
5
|
import { DEFAULT_PRESENTATION_CONFIG, mergePresentationConfigLayers, resolvePresentationMode } from '../presentation/config.js';
|
|
4
6
|
import { loadPresentationConfigLayers, resetPresentationConfigScope, savePresentationConfigScope } from '../presentation/config-store.js';
|
|
5
7
|
import { resolveBrowserExecutable } from '../fetch/browser-resolution.js';
|
|
8
|
+
import { getLatestChangelogEntry } from '../changelog-notice.js';
|
|
6
9
|
const PRESENTATION_TOOL_NAMES = ['web_explore'];
|
|
7
10
|
function parseScopeToken(token) {
|
|
8
11
|
return token === 'global' || token === 'project' ? token : undefined;
|
|
@@ -22,6 +25,19 @@ async function defaultCheckTypebox() {
|
|
|
22
25
|
return false;
|
|
23
26
|
}
|
|
24
27
|
}
|
|
28
|
+
function formatBackendSummary(config = DEFAULT_BACKEND_CONFIG) {
|
|
29
|
+
const search = config.search.baseUrl
|
|
30
|
+
? `search: ${config.search.provider} (${config.search.baseUrl})`
|
|
31
|
+
: `search: ${config.search.provider}`;
|
|
32
|
+
const fetch = config.fetch.baseUrl
|
|
33
|
+
? `fetch: ${config.fetch.provider} (${config.fetch.baseUrl})`
|
|
34
|
+
: `fetch: ${config.fetch.provider}`;
|
|
35
|
+
return [
|
|
36
|
+
search,
|
|
37
|
+
fetch,
|
|
38
|
+
`headless: ${config.headless.provider}`
|
|
39
|
+
].join('\n');
|
|
40
|
+
}
|
|
25
41
|
function formatConfigSummary(config) {
|
|
26
42
|
const lines = [`defaultMode: ${config.defaultMode}`];
|
|
27
43
|
for (const toolName of PRESENTATION_TOOL_NAMES) {
|
|
@@ -233,6 +249,8 @@ export function registerWebAgentConfigCommands(pi, deps = {}) {
|
|
|
233
249
|
arch: process.arch
|
|
234
250
|
};
|
|
235
251
|
const checkTypebox = deps.checkTypebox ?? defaultCheckTypebox;
|
|
252
|
+
const checkBackends = deps.checkBackends ?? ((config) => checkBackendHealth(config));
|
|
253
|
+
const getChangelog = deps.getChangelog ?? (() => getLatestChangelogEntry());
|
|
236
254
|
pi.registerCommand('web-agent', {
|
|
237
255
|
description: 'Open settings or manage pi-web-agent presentation config',
|
|
238
256
|
handler: async (args, ctx) => {
|
|
@@ -254,11 +272,17 @@ export function registerWebAgentConfigCommands(pi, deps = {}) {
|
|
|
254
272
|
}
|
|
255
273
|
}
|
|
256
274
|
if (action === 'doctor') {
|
|
257
|
-
const [typeboxOk, browser] = await Promise.all([checkTypebox(), resolveBrowser()]);
|
|
275
|
+
const [typeboxOk, browser, loaded] = await Promise.all([checkTypebox(), resolveBrowser(), load()]);
|
|
276
|
+
const backendConfig = loaded.effectiveBackends ?? DEFAULT_BACKEND_CONFIG;
|
|
277
|
+
const backendIssues = validateBackendConfig(backendConfig);
|
|
278
|
+
const backendHealth = await checkBackends(backendConfig);
|
|
258
279
|
const lines = [
|
|
259
280
|
'pi-web-agent: loaded',
|
|
260
281
|
`runtime: node ${runtime.nodeVersion} ${runtime.platform} ${runtime.arch}`,
|
|
261
|
-
`typebox: ${typeboxOk ? 'ok' : 'missing'}
|
|
282
|
+
`typebox: ${typeboxOk ? 'ok' : 'missing'}`,
|
|
283
|
+
formatBackendSummary(backendConfig),
|
|
284
|
+
backendIssues.length > 0 ? `backend config: warning\n${backendIssues.join('\n')}` : 'backend config: ok',
|
|
285
|
+
...backendHealth
|
|
262
286
|
];
|
|
263
287
|
if (browser.ok) {
|
|
264
288
|
lines.push(`browser: ${browser.browser} ${browser.executablePath}`);
|
|
@@ -275,11 +299,17 @@ export function registerWebAgentConfigCommands(pi, deps = {}) {
|
|
|
275
299
|
const loaded = await load();
|
|
276
300
|
ctx.ui.notify([
|
|
277
301
|
formatConfigSummary(loaded.effectiveConfig),
|
|
302
|
+
formatBackendSummary(loaded.effectiveBackends ?? DEFAULT_BACKEND_CONFIG),
|
|
278
303
|
`global: ${loaded.global.path}${loaded.global.exists ? '' : ' (missing)'}`,
|
|
279
304
|
`project: ${loaded.project.path}${loaded.project.exists ? '' : ' (missing)'}`
|
|
280
305
|
].join('\n'), 'info');
|
|
281
306
|
return;
|
|
282
307
|
}
|
|
308
|
+
if (action === 'changelog') {
|
|
309
|
+
const changelog = await getChangelog();
|
|
310
|
+
ctx.ui.notify(changelog ?? 'No pi-web-agent changelog entries found.', 'info');
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
283
313
|
if (action === 'reset') {
|
|
284
314
|
const scope = parseScopeToken(maybeScope) ?? 'project';
|
|
285
315
|
await reset(scope);
|
|
@@ -328,7 +358,7 @@ export function registerWebAgentConfigCommands(pi, deps = {}) {
|
|
|
328
358
|
ctx.ui.notify(`Saved ${result.scope} config`, 'info');
|
|
329
359
|
return;
|
|
330
360
|
}
|
|
331
|
-
ctx.ui.notify('Use /web-agent, /web-agent show, /web-agent doctor, /web-agent reset project, or /web-agent settings', 'info');
|
|
361
|
+
ctx.ui.notify('Use /web-agent, /web-agent show, /web-agent doctor, /web-agent changelog, /web-agent reset project, or /web-agent settings', 'info');
|
|
332
362
|
}
|
|
333
363
|
});
|
|
334
364
|
}
|
package/dist/extension.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { ExtensionAPI } from '@
|
|
1
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
export default function extension(pi: ExtensionAPI): void;
|
package/dist/extension.js
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
|
+
import { DEFAULT_BACKEND_CONFIG } from './backends/config.js';
|
|
1
2
|
import { Type } from 'typebox';
|
|
2
3
|
import { registerWebAgentConfigCommands } from './commands/web-agent-config.js';
|
|
3
4
|
import { DEFAULT_PRESENTATION_CONFIG, resolvePresentationMode } from './presentation/config.js';
|
|
5
|
+
import { createResearchWorkflow } from './orchestration/index.js';
|
|
4
6
|
import { loadPresentationConfigLayers } from './presentation/config-store.js';
|
|
5
7
|
import { selectPresentationView } from './presentation/select-view.js';
|
|
6
8
|
import { createWebExploreTool } from './tools/web-explore.js';
|
|
7
|
-
|
|
9
|
+
import { getUpdateChangelogNotice } from './changelog-notice.js';
|
|
10
|
+
async function loadWebAgentConfig(pi) {
|
|
8
11
|
const store = pi.__presentationConfigStore;
|
|
12
|
+
return store?.load?.() ?? loadPresentationConfigLayers();
|
|
13
|
+
}
|
|
14
|
+
async function getEffectivePresentationConfig(pi) {
|
|
9
15
|
try {
|
|
10
|
-
const loaded = await (
|
|
16
|
+
const loaded = await loadWebAgentConfig(pi);
|
|
11
17
|
return loaded.effectiveConfig;
|
|
12
18
|
}
|
|
13
19
|
catch {
|
|
14
20
|
return DEFAULT_PRESENTATION_CONFIG;
|
|
15
21
|
}
|
|
16
22
|
}
|
|
23
|
+
async function getEffectiveBackendConfig(pi) {
|
|
24
|
+
try {
|
|
25
|
+
const loaded = await loadWebAgentConfig(pi);
|
|
26
|
+
return loaded.effectiveBackends ?? DEFAULT_BACKEND_CONFIG;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return DEFAULT_BACKEND_CONFIG;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
17
32
|
async function renderToolText(pi, toolName, details) {
|
|
18
33
|
const config = await getEffectivePresentationConfig(pi);
|
|
19
34
|
const mode = resolvePresentationMode(toolName, config);
|
|
@@ -21,8 +36,33 @@ async function renderToolText(pi, toolName, details) {
|
|
|
21
36
|
}
|
|
22
37
|
export default function extension(pi) {
|
|
23
38
|
registerWebAgentConfigCommands(pi);
|
|
24
|
-
const
|
|
25
|
-
|
|
39
|
+
const injectedWebExplore = pi.__webExploreTool;
|
|
40
|
+
let cachedBackendKey;
|
|
41
|
+
let cachedWebExplore;
|
|
42
|
+
async function getConfiguredWebExplore() {
|
|
43
|
+
if (injectedWebExplore)
|
|
44
|
+
return injectedWebExplore;
|
|
45
|
+
const backendConfig = await getEffectiveBackendConfig(pi);
|
|
46
|
+
const backendKey = JSON.stringify(backendConfig);
|
|
47
|
+
if (!cachedWebExplore || cachedBackendKey !== backendKey) {
|
|
48
|
+
cachedBackendKey = backendKey;
|
|
49
|
+
cachedWebExplore = createWebExploreTool({
|
|
50
|
+
explore: createResearchWorkflow({ backendConfig })
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return cachedWebExplore;
|
|
54
|
+
}
|
|
55
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
56
|
+
try {
|
|
57
|
+
const notice = await getUpdateChangelogNotice();
|
|
58
|
+
if (notice) {
|
|
59
|
+
ctx.ui.notify(`pi-web-agent updated\n\n${notice}`, 'info');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Never block extension startup on changelog display.
|
|
64
|
+
}
|
|
65
|
+
});
|
|
26
66
|
pi.on('before_agent_start', async (event) => ({
|
|
27
67
|
systemPrompt: `${event.systemPrompt}\n\n` +
|
|
28
68
|
'For web research questions that require finding and comparing sources, use web_explore. ' +
|
|
@@ -37,6 +77,7 @@ export default function extension(pi) {
|
|
|
37
77
|
query: Type.String({ description: 'Web research question to explore.' })
|
|
38
78
|
}),
|
|
39
79
|
async execute(_toolCallId, params) {
|
|
80
|
+
const webExplore = await getConfiguredWebExplore();
|
|
40
81
|
const result = await webExplore({ query: params.query });
|
|
41
82
|
return {
|
|
42
83
|
content: [
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
function buildScrapeUrl(baseUrl) {
|
|
2
|
+
return new URL('/v1/scrape', baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`).toString();
|
|
3
|
+
}
|
|
4
|
+
function errorMessage(error) {
|
|
5
|
+
return error instanceof Error ? error.message : String(error);
|
|
6
|
+
}
|
|
7
|
+
export function createFirecrawlFetcher({ baseUrl, apiKey, fetchImpl = fetch }) {
|
|
8
|
+
return async function firecrawlFetch(url) {
|
|
9
|
+
try {
|
|
10
|
+
const headers = { 'content-type': 'application/json' };
|
|
11
|
+
if (apiKey) {
|
|
12
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
13
|
+
}
|
|
14
|
+
const response = await fetchImpl(buildScrapeUrl(baseUrl), {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers,
|
|
17
|
+
body: JSON.stringify({ url, formats: ['markdown'] })
|
|
18
|
+
});
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
throw new Error(`HTTP ${response.status}`);
|
|
21
|
+
}
|
|
22
|
+
const parsed = (await response.json());
|
|
23
|
+
if (parsed.success === false) {
|
|
24
|
+
throw new Error(typeof parsed.error === 'string' ? parsed.error : 'Firecrawl scrape failed.');
|
|
25
|
+
}
|
|
26
|
+
const text = typeof parsed.data?.markdown === 'string'
|
|
27
|
+
? parsed.data.markdown
|
|
28
|
+
: typeof parsed.data?.html === 'string'
|
|
29
|
+
? parsed.data.html
|
|
30
|
+
: '';
|
|
31
|
+
const resolvedUrl = typeof parsed.data?.metadata?.sourceURL === 'string'
|
|
32
|
+
? parsed.data.metadata.sourceURL
|
|
33
|
+
: url;
|
|
34
|
+
const title = typeof parsed.data?.metadata?.title === 'string'
|
|
35
|
+
? parsed.data.metadata.title
|
|
36
|
+
: undefined;
|
|
37
|
+
if (!text.trim()) {
|
|
38
|
+
return {
|
|
39
|
+
status: 'needs_headless',
|
|
40
|
+
url: resolvedUrl,
|
|
41
|
+
metadata: { method: 'firecrawl', cacheHit: false },
|
|
42
|
+
error: { code: 'WEAK_EXTRACTION', message: 'Firecrawl did not return useful page text.' }
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
status: 'ok',
|
|
47
|
+
url: resolvedUrl,
|
|
48
|
+
content: { title, text },
|
|
49
|
+
metadata: { method: 'firecrawl', cacheHit: false, truncated: text.length >= 4000 }
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
return {
|
|
54
|
+
status: 'error',
|
|
55
|
+
url,
|
|
56
|
+
metadata: { method: 'firecrawl', cacheHit: false },
|
|
57
|
+
error: { code: 'FETCH_FAILED', message: `Firecrawl scrape failed: ${errorMessage(error)}` }
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { BackendConfig } from '../backends/config.js';
|
|
1
2
|
import type { WebFetchHeadlessResponse, WebFetchResponse, WebSearchResponse } from '../types.js';
|
|
2
|
-
export declare function createResearchWorkflow({ search, fetchPage, headlessFetch }?: {
|
|
3
|
+
export declare function createResearchWorkflow({ backendConfig, search, fetchPage, headlessFetch }?: {
|
|
4
|
+
backendConfig?: BackendConfig;
|
|
3
5
|
search?: (input: {
|
|
4
6
|
query: string;
|
|
5
7
|
}) => Promise<WebSearchResponse>;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createWebFetchTool } from '../tools/web-fetch.js';
|
|
3
|
-
import { createWebSearchTool } from '../tools/web-search.js';
|
|
1
|
+
import { createBackendSet } from '../backends/factory.js';
|
|
4
2
|
import { createResearchOrchestrator } from './research-orchestrator.js';
|
|
5
3
|
import { createResearchWorker } from './research-worker.js';
|
|
6
|
-
export function createResearchWorkflow({ search
|
|
7
|
-
const
|
|
8
|
-
|
|
4
|
+
export function createResearchWorkflow({ backendConfig, search, fetchPage, headlessFetch } = {}) {
|
|
5
|
+
const backends = createBackendSet(backendConfig);
|
|
6
|
+
const resolvedSearch = search ?? backends.search;
|
|
7
|
+
const resolvedFetchPage = fetchPage ?? backends.fetchPage;
|
|
8
|
+
const resolvedHeadlessFetch = headlessFetch ?? backends.headlessFetch;
|
|
9
|
+
const worker = createResearchWorker({ search: resolvedSearch, fetchPage: resolvedFetchPage });
|
|
10
|
+
return createResearchOrchestrator({ worker, headlessFetch: resolvedHeadlessFetch });
|
|
9
11
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type ResearchSourceKind = 'official-docs' | 'official-api' | 'official-discussion' | 'community' | 'issue-thread' | 'package-page' | 'other';
|
|
2
|
-
export type ResearchMethod = 'search' | 'http' | 'headless';
|
|
2
|
+
export type ResearchMethod = 'search' | 'http' | 'headless' | 'firecrawl';
|
|
3
3
|
export type ResearchEvidence = {
|
|
4
4
|
title: string;
|
|
5
5
|
url: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type BackendConfig, type BackendConfigOverride } from '../backends/config.js';
|
|
1
2
|
import type { PresentationConfig, PresentationConfigOverride, PresentationScope } from './types.js';
|
|
2
3
|
export type PresentationConfigStoreOptions = {
|
|
3
4
|
homeDir?: string;
|
|
@@ -7,12 +8,14 @@ export type PresentationConfigLayer = {
|
|
|
7
8
|
path: string;
|
|
8
9
|
exists: boolean;
|
|
9
10
|
rawConfig?: PresentationConfigOverride;
|
|
11
|
+
rawBackends?: BackendConfigOverride;
|
|
10
12
|
error?: string;
|
|
11
13
|
};
|
|
12
14
|
export type LoadedPresentationConfig = {
|
|
13
15
|
global: PresentationConfigLayer;
|
|
14
16
|
project: PresentationConfigLayer;
|
|
15
17
|
effectiveConfig: PresentationConfig;
|
|
18
|
+
effectiveBackends: BackendConfig;
|
|
16
19
|
};
|
|
17
20
|
export declare function getPresentationConfigPaths(options?: PresentationConfigStoreOptions): {
|
|
18
21
|
globalPath: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { DEFAULT_BACKEND_CONFIG, extractBackendConfigOverride, mergeBackendConfigLayers } from '../backends/config.js';
|
|
3
4
|
import { DEFAULT_PRESENTATION_CONFIG, extractPresentationConfigOverride, mergePresentationConfigLayers } from './config.js';
|
|
4
5
|
export function getPresentationConfigPaths(options = {}) {
|
|
5
6
|
const homeDir = options.homeDir ?? process.env.USERPROFILE ?? process.env.HOME ?? '';
|
|
@@ -9,14 +10,21 @@ export function getPresentationConfigPaths(options = {}) {
|
|
|
9
10
|
projectPath: path.join(projectDir, '.pi', 'extensions', 'pi-web-agent', 'config.json')
|
|
10
11
|
};
|
|
11
12
|
}
|
|
13
|
+
function hasPresentationRoot(parsed) {
|
|
14
|
+
return parsed.presentation !== undefined;
|
|
15
|
+
}
|
|
12
16
|
async function readPresentationConfigFile(filePath) {
|
|
13
17
|
try {
|
|
14
18
|
const rawText = await readFile(filePath, 'utf8');
|
|
15
19
|
const parsed = JSON.parse(rawText);
|
|
20
|
+
const presentationFile = hasPresentationRoot(parsed)
|
|
21
|
+
? parsed
|
|
22
|
+
: { presentation: parsed };
|
|
16
23
|
return {
|
|
17
24
|
path: filePath,
|
|
18
25
|
exists: true,
|
|
19
|
-
rawConfig: extractPresentationConfigOverride(
|
|
26
|
+
rawConfig: extractPresentationConfigOverride(presentationFile),
|
|
27
|
+
rawBackends: extractBackendConfigOverride(parsed)
|
|
20
28
|
};
|
|
21
29
|
}
|
|
22
30
|
catch (error) {
|
|
@@ -48,7 +56,8 @@ export async function loadPresentationConfigLayers(options = {}) {
|
|
|
48
56
|
return {
|
|
49
57
|
global,
|
|
50
58
|
project,
|
|
51
|
-
effectiveConfig: mergePresentationConfigLayers(DEFAULT_PRESENTATION_CONFIG, global.rawConfig, project.rawConfig)
|
|
59
|
+
effectiveConfig: mergePresentationConfigLayers(DEFAULT_PRESENTATION_CONFIG, global.rawConfig, project.rawConfig),
|
|
60
|
+
effectiveBackends: mergeBackendConfigLayers(DEFAULT_BACKEND_CONFIG, global.rawBackends, project.rawBackends)
|
|
52
61
|
};
|
|
53
62
|
}
|
|
54
63
|
export async function savePresentationConfigScope(options, scope, config) {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { buildSearchPresentation } from '../presentation/search-presentation.js';
|
|
2
|
+
function buildSearchUrl(baseUrl, query) {
|
|
3
|
+
const url = new URL('/search', baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);
|
|
4
|
+
url.searchParams.set('q', query);
|
|
5
|
+
url.searchParams.set('format', 'json');
|
|
6
|
+
return url.toString();
|
|
7
|
+
}
|
|
8
|
+
function normalizeResults(response) {
|
|
9
|
+
return (response.results ?? []).flatMap((result) => {
|
|
10
|
+
if (typeof result.title !== 'string' || typeof result.url !== 'string') {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
title: result.title,
|
|
16
|
+
url: result.url,
|
|
17
|
+
snippet: typeof result.content === 'string' ? result.content : ''
|
|
18
|
+
}
|
|
19
|
+
];
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function createSearxngSearchTool({ baseUrl, fetchImpl = fetch }) {
|
|
23
|
+
return async function searxngSearch({ query }) {
|
|
24
|
+
const normalizedQuery = query.trim();
|
|
25
|
+
if (!normalizedQuery) {
|
|
26
|
+
const result = {
|
|
27
|
+
status: 'error',
|
|
28
|
+
results: [],
|
|
29
|
+
metadata: { backend: 'searxng', cacheHit: false },
|
|
30
|
+
error: { code: 'INVALID_QUERY', message: 'Query must not be empty.' }
|
|
31
|
+
};
|
|
32
|
+
return { ...result, presentation: buildSearchPresentation(result) };
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetchImpl(buildSearchUrl(baseUrl, normalizedQuery));
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error(`HTTP ${response.status}`);
|
|
38
|
+
}
|
|
39
|
+
const parsed = (await response.json());
|
|
40
|
+
const results = normalizeResults(parsed);
|
|
41
|
+
const result = results.length > 0
|
|
42
|
+
? {
|
|
43
|
+
status: 'ok',
|
|
44
|
+
results,
|
|
45
|
+
metadata: { backend: 'searxng', cacheHit: false }
|
|
46
|
+
}
|
|
47
|
+
: {
|
|
48
|
+
status: 'error',
|
|
49
|
+
results: [],
|
|
50
|
+
metadata: { backend: 'searxng', cacheHit: false },
|
|
51
|
+
error: { code: 'NO_RESULTS', message: 'SearXNG returned no usable results for this query.' }
|
|
52
|
+
};
|
|
53
|
+
return { ...result, presentation: buildSearchPresentation(result) };
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
57
|
+
const result = {
|
|
58
|
+
status: 'error',
|
|
59
|
+
results: [],
|
|
60
|
+
metadata: { backend: 'searxng', cacheHit: false },
|
|
61
|
+
error: { code: 'FETCH_FAILED', message: `SearXNG search request failed: ${rawMessage}` }
|
|
62
|
+
};
|
|
63
|
+
return { ...result, presentation: buildSearchPresentation(result) };
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -11,11 +11,11 @@ export type ToolError = {
|
|
|
11
11
|
message: string;
|
|
12
12
|
};
|
|
13
13
|
export type SearchMetadata = {
|
|
14
|
-
backend: 'duckduckgo';
|
|
14
|
+
backend: 'duckduckgo' | 'searxng';
|
|
15
15
|
cacheHit: boolean;
|
|
16
16
|
};
|
|
17
17
|
export type FetchMetadata = {
|
|
18
|
-
method: 'http' | 'headless';
|
|
18
|
+
method: 'http' | 'headless' | 'firecrawl';
|
|
19
19
|
cacheHit: boolean;
|
|
20
20
|
contentType?: string;
|
|
21
21
|
truncated?: boolean;
|
|
@@ -56,7 +56,7 @@ export type WebExploreResponse = {
|
|
|
56
56
|
sources: Array<{
|
|
57
57
|
title: string;
|
|
58
58
|
url: string;
|
|
59
|
-
method?: 'http' | 'headless';
|
|
59
|
+
method?: 'http' | 'headless' | 'firecrawl';
|
|
60
60
|
}>;
|
|
61
61
|
caveat?: string;
|
|
62
62
|
metadata?: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@demigodmode/pi-web-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
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",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"dist",
|
|
16
|
-
"README.md"
|
|
16
|
+
"README.md",
|
|
17
|
+
"CHANGELOG.md"
|
|
17
18
|
],
|
|
18
19
|
"keywords": [
|
|
19
20
|
"pi-package",
|
|
@@ -60,7 +61,8 @@
|
|
|
60
61
|
"typebox": "^1.1.37"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
63
|
-
"@
|
|
64
|
+
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
65
|
+
"@earendil-works/pi-tui": "^0.74.0",
|
|
64
66
|
"@types/jsdom": "^21.1.7",
|
|
65
67
|
"@types/node": "^24.0.0",
|
|
66
68
|
"@vitest/coverage-v8": "^3.2.4",
|
|
@@ -69,6 +71,7 @@
|
|
|
69
71
|
"vitest": "^3.2.0"
|
|
70
72
|
},
|
|
71
73
|
"peerDependencies": {
|
|
72
|
-
"@
|
|
74
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
75
|
+
"@earendil-works/pi-tui": "*"
|
|
73
76
|
}
|
|
74
77
|
}
|