@demigodmode/pi-web-agent 1.1.0 → 1.3.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 +30 -0
- package/README.md +14 -4
- package/dist/backends/doctor.js +3 -0
- package/dist/commands/web-agent-config.d.ts +14 -1
- package/dist/commands/web-agent-config.js +315 -15
- package/dist/fetch/headless-fetch.d.ts +2 -1
- package/dist/fetch/headless-fetch.js +11 -7
- package/dist/orchestration/candidate-selector.d.ts +2 -1
- package/dist/orchestration/candidate-selector.js +28 -9
- package/dist/orchestration/direct-url.d.ts +1 -0
- package/dist/orchestration/direct-url.js +47 -0
- package/dist/orchestration/index.js +5 -1
- package/dist/orchestration/research-orchestrator.d.ts +5 -2
- package/dist/orchestration/research-orchestrator.js +97 -27
- package/dist/orchestration/research-worker.js +3 -13
- package/dist/orchestration/source-profile.d.ts +8 -0
- package/dist/orchestration/source-profile.js +60 -0
- package/dist/presentation/config-store.d.ts +1 -0
- package/dist/presentation/config-store.js +43 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,36 @@ The format is intentionally simple and release-oriented.
|
|
|
18
18
|
### Breaking
|
|
19
19
|
- None.
|
|
20
20
|
|
|
21
|
+
## [1.3.0] - 2026-06-04
|
|
22
|
+
### Added
|
|
23
|
+
- Added direct URL handling in `web_explore` so linked pages are read before search results.
|
|
24
|
+
- Added forum/thread source classification for Reddit-style discussions, forums, Stack Overflow, and GitHub issues/discussions.
|
|
25
|
+
- Added Playwright-managed Chromium fallback when no local Chromium-family browser is detected.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- Discussion-oriented queries now prefer forum/thread results over generic pages.
|
|
29
|
+
- `/web-agent doctor` now reports the local-browser headless backend and managed Chromium fallback.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- Preserved direct/thread fetch gaps in bounded research results so unreadable thread sources get explicit caveats.
|
|
33
|
+
|
|
34
|
+
### Breaking
|
|
35
|
+
- None.
|
|
36
|
+
|
|
37
|
+
## [1.2.0] - 2026-06-01
|
|
38
|
+
### Added
|
|
39
|
+
- Added backend provider and fallback editing to `/web-agent settings`.
|
|
40
|
+
- Added interactive URL prompts for SearXNG and Firecrawl backend setup.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- Nothing yet.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
- Nothing yet.
|
|
47
|
+
|
|
48
|
+
### Breaking
|
|
49
|
+
- None.
|
|
50
|
+
|
|
21
51
|
## [1.1.0] - 2026-05-25
|
|
22
52
|
### Added
|
|
23
53
|
- Added explicit opt-in fallback from SearXNG to DuckDuckGo and Firecrawl to HTTP.
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
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 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, and caveats internally.
|
|
12
12
|
|
|
13
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.
|
|
14
14
|
|
|
@@ -20,9 +20,9 @@ Compatibility notice: current `pi-web-agent` requires Pi 0.74+ because Pi packag
|
|
|
20
20
|
pi install npm:@demigodmode/pi-web-agent
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
After installing, reload or restart Pi. Run `/web-agent` for the action menu, or `/web-agent doctor` to check whether the package loaded cleanly and
|
|
23
|
+
After installing, reload or restart Pi. Run `/web-agent` for the action menu, or `/web-agent doctor` to check whether the package loaded cleanly and which web backends are configured.
|
|
24
24
|
|
|
25
|
-
Headless rendering
|
|
25
|
+
Headless rendering first tries a detectable Chromium-family browser: Chrome, Chromium, Edge, or Brave. If none is found, it falls back to Playwright-managed Chromium and still launches headless. Firefox/Safari-only systems can still use search and plain HTTP reads; browser-rendered fallback uses Chromium.
|
|
26
26
|
|
|
27
27
|
Later on, update installed packages with:
|
|
28
28
|
|
|
@@ -109,7 +109,17 @@ Example:
|
|
|
109
109
|
}
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
Backend config is also supported. Defaults remain DuckDuckGo search, plain HTTP fetch, and local
|
|
112
|
+
Backend config is also supported. Defaults remain DuckDuckGo search, plain HTTP fetch, and local-browser headless fallback with managed Chromium fallback configured.
|
|
113
|
+
|
|
114
|
+
Backend settings can be changed from:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
/web-agent settings
|
|
118
|
+
```
|
|
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.
|
|
121
|
+
|
|
122
|
+
If you already run SearXNG or Firecrawl, see the self-hosted backend guide:
|
|
113
123
|
|
|
114
124
|
- https://demigodmode.github.io/pi-web-agent/self-hosted-backends
|
|
115
125
|
|
package/dist/backends/doctor.js
CHANGED
|
@@ -83,5 +83,8 @@ export async function checkBackendHealth(config, { fetchImpl = fetch, timeoutMs
|
|
|
83
83
|
if (config.fetch.fallback) {
|
|
84
84
|
lines.push(`fetch fallback: ${config.fetch.fallback}`);
|
|
85
85
|
}
|
|
86
|
+
if (config.headless.provider === 'local-browser') {
|
|
87
|
+
lines.push('headless backend: local-browser (managed Chromium fallback configured)');
|
|
88
|
+
}
|
|
86
89
|
return lines;
|
|
87
90
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BackendConfig } from '../backends/config.js';
|
|
1
|
+
import { type BackendConfig, type BackendConfigOverride } from '../backends/config.js';
|
|
2
2
|
import { type ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
3
3
|
import { loadPresentationConfigLayers, type LoadedPresentationConfig } from '../presentation/config-store.js';
|
|
4
4
|
import { type BrowserResolutionResult } from '../fetch/browser-resolution.js';
|
|
@@ -6,6 +6,7 @@ import type { PresentationConfig, PresentationConfigOverride, PresentationScope
|
|
|
6
6
|
type CommandDeps = {
|
|
7
7
|
load?: () => ReturnType<typeof loadPresentationConfigLayers>;
|
|
8
8
|
save?: (scope: PresentationScope, config: PresentationConfigOverride) => Promise<void>;
|
|
9
|
+
saveBackends?: (scope: PresentationScope, config: BackendConfigOverride) => Promise<void>;
|
|
9
10
|
reset?: (scope: PresentationScope) => Promise<void>;
|
|
10
11
|
resolveBrowser?: () => Promise<BrowserResolutionResult>;
|
|
11
12
|
runtime?: {
|
|
@@ -20,13 +21,25 @@ type CommandDeps = {
|
|
|
20
21
|
export type SettingsDraftState = {
|
|
21
22
|
scope: PresentationScope;
|
|
22
23
|
drafts: Record<PresentationScope, PresentationConfig>;
|
|
24
|
+
backendDrafts: Record<PresentationScope, BackendConfig>;
|
|
23
25
|
config: PresentationConfig;
|
|
26
|
+
backends: BackendConfig;
|
|
27
|
+
};
|
|
28
|
+
export declare function validateBackendUrl(value: string): {
|
|
29
|
+
ok: true;
|
|
30
|
+
value: string;
|
|
31
|
+
} | {
|
|
32
|
+
ok: false;
|
|
33
|
+
message: string;
|
|
24
34
|
};
|
|
25
35
|
export declare function getInheritedConfigForScope(loaded: Awaited<LoadedPresentationConfig>, scope: PresentationScope): PresentationConfig;
|
|
26
36
|
export declare function getScopeDisplayConfig(loaded: Awaited<LoadedPresentationConfig>, scope: PresentationScope): PresentationConfig;
|
|
37
|
+
export declare function getInheritedBackendsForScope(loaded: Awaited<LoadedPresentationConfig>, scope: PresentationScope): BackendConfig;
|
|
38
|
+
export declare function getScopeDisplayBackends(loaded: Awaited<LoadedPresentationConfig>, scope: PresentationScope): BackendConfig;
|
|
27
39
|
export declare function createSettingsDraftState(loaded: Awaited<LoadedPresentationConfig>, initialScope: PresentationScope): SettingsDraftState;
|
|
28
40
|
export declare function applySettingsValue(state: SettingsDraftState, id: string, newValue: string): SettingsDraftState;
|
|
29
41
|
export declare function collapsePresentationConfigToOverride(config: PresentationConfig, inheritedConfig: PresentationConfig): PresentationConfigOverride;
|
|
42
|
+
export declare function collapseBackendConfigToOverride(config: BackendConfig, inheritedConfig: BackendConfig): BackendConfigOverride;
|
|
30
43
|
export declare function handleSettingsShortcut(data: string): {
|
|
31
44
|
action: 'cancel' | 'reset' | 'save';
|
|
32
45
|
} | undefined;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { DEFAULT_BACKEND_CONFIG, validateBackendConfig } from '../backends/config.js';
|
|
1
|
+
import { DEFAULT_BACKEND_CONFIG, mergeBackendConfigLayers, validateBackendConfig } from '../backends/config.js';
|
|
2
2
|
import { checkBackendHealth } from '../backends/doctor.js';
|
|
3
3
|
import { DynamicBorder, getSettingsListTheme } from '@earendil-works/pi-coding-agent';
|
|
4
4
|
import { Container, SelectList, SettingsList, Text } from '@earendil-works/pi-tui';
|
|
5
5
|
import { DEFAULT_PRESENTATION_CONFIG, mergePresentationConfigLayers, resolvePresentationMode } from '../presentation/config.js';
|
|
6
|
-
import { loadPresentationConfigLayers, resetPresentationConfigScope, savePresentationConfigScope } from '../presentation/config-store.js';
|
|
6
|
+
import { loadPresentationConfigLayers, resetPresentationConfigScope, saveBackendConfigScope, savePresentationConfigScope } from '../presentation/config-store.js';
|
|
7
7
|
import { resolveBrowserExecutable } from '../fetch/browser-resolution.js';
|
|
8
8
|
import { getLatestChangelogEntry } from '../changelog-notice.js';
|
|
9
9
|
const PRESENTATION_TOOL_NAMES = ['web_explore'];
|
|
@@ -16,6 +16,34 @@ function clonePresentationConfig(config) {
|
|
|
16
16
|
tools: { ...config.tools }
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
+
function cloneBackendConfig(config) {
|
|
20
|
+
return {
|
|
21
|
+
search: {
|
|
22
|
+
...config.search,
|
|
23
|
+
options: config.search.options ? { ...config.search.options } : undefined
|
|
24
|
+
},
|
|
25
|
+
fetch: {
|
|
26
|
+
...config.fetch,
|
|
27
|
+
options: config.fetch.options ? { ...config.fetch.options } : undefined
|
|
28
|
+
},
|
|
29
|
+
headless: { ...config.headless }
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function sameJson(left, right) {
|
|
33
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
34
|
+
}
|
|
35
|
+
export function validateBackendUrl(value) {
|
|
36
|
+
try {
|
|
37
|
+
const url = new URL(value.trim());
|
|
38
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
39
|
+
return { ok: false, message: 'Invalid URL. Include http:// or https://.' };
|
|
40
|
+
}
|
|
41
|
+
return { ok: true, value: url.toString().replace(/\/$/, '') };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return { ok: false, message: 'Invalid URL. Include http:// or https://.' };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
19
47
|
async function defaultCheckTypebox() {
|
|
20
48
|
try {
|
|
21
49
|
await import('typebox');
|
|
@@ -62,7 +90,7 @@ function formatConfigSummary(config) {
|
|
|
62
90
|
}
|
|
63
91
|
return lines.join('\n');
|
|
64
92
|
}
|
|
65
|
-
function
|
|
93
|
+
function buildPresentationSettingsItems(scope, config) {
|
|
66
94
|
return [
|
|
67
95
|
{
|
|
68
96
|
id: 'scope',
|
|
@@ -84,6 +112,58 @@ function buildSettingsItems(scope, config) {
|
|
|
84
112
|
}))
|
|
85
113
|
];
|
|
86
114
|
}
|
|
115
|
+
function buildBackendSettingsItems(scope, backends) {
|
|
116
|
+
return [
|
|
117
|
+
{
|
|
118
|
+
id: 'scope',
|
|
119
|
+
label: 'Write scope',
|
|
120
|
+
currentValue: scope,
|
|
121
|
+
values: ['project', 'global']
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 'backend:search:provider',
|
|
125
|
+
label: 'Search backend',
|
|
126
|
+
currentValue: backends.search.provider,
|
|
127
|
+
values: ['duckduckgo', 'searxng']
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'backend:search:baseUrl',
|
|
131
|
+
label: 'SearXNG URL',
|
|
132
|
+
currentValue: backends.search.baseUrl ?? 'not set',
|
|
133
|
+
values: ['edit']
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: 'backend:search:fallback',
|
|
137
|
+
label: 'SearXNG fallback',
|
|
138
|
+
currentValue: backends.search.provider === 'searxng' ? backends.search.fallback ?? 'off' : 'off',
|
|
139
|
+
values: backends.search.provider === 'searxng' ? ['off', 'duckduckgo'] : ['off']
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: 'backend:fetch:provider',
|
|
143
|
+
label: 'Fetch backend',
|
|
144
|
+
currentValue: backends.fetch.provider,
|
|
145
|
+
values: ['http', 'firecrawl']
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 'backend:fetch:baseUrl',
|
|
149
|
+
label: 'Firecrawl URL',
|
|
150
|
+
currentValue: backends.fetch.baseUrl ?? 'not set',
|
|
151
|
+
values: ['edit']
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'backend:fetch:fallback',
|
|
155
|
+
label: 'Firecrawl fallback',
|
|
156
|
+
currentValue: backends.fetch.provider === 'firecrawl' ? backends.fetch.fallback ?? 'off' : 'off',
|
|
157
|
+
values: backends.fetch.provider === 'firecrawl' ? ['off', 'http'] : ['off']
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: 'backend:secret:firecrawl',
|
|
161
|
+
label: 'Firecrawl API key',
|
|
162
|
+
currentValue: 'env var',
|
|
163
|
+
values: ['env var']
|
|
164
|
+
}
|
|
165
|
+
];
|
|
166
|
+
}
|
|
87
167
|
function isToolName(value) {
|
|
88
168
|
return PRESENTATION_TOOL_NAMES.includes(value);
|
|
89
169
|
}
|
|
@@ -102,15 +182,33 @@ export function getScopeDisplayConfig(loaded, scope) {
|
|
|
102
182
|
}
|
|
103
183
|
return mergePresentationConfigLayers(DEFAULT_PRESENTATION_CONFIG, loaded.global.rawConfig, loaded.project.rawConfig);
|
|
104
184
|
}
|
|
185
|
+
export function getInheritedBackendsForScope(loaded, scope) {
|
|
186
|
+
if (scope === 'global') {
|
|
187
|
+
return DEFAULT_BACKEND_CONFIG;
|
|
188
|
+
}
|
|
189
|
+
return mergeBackendConfigLayers(DEFAULT_BACKEND_CONFIG, loaded.global.rawBackends);
|
|
190
|
+
}
|
|
191
|
+
export function getScopeDisplayBackends(loaded, scope) {
|
|
192
|
+
if (scope === 'global') {
|
|
193
|
+
return mergeBackendConfigLayers(DEFAULT_BACKEND_CONFIG, loaded.global.rawBackends);
|
|
194
|
+
}
|
|
195
|
+
return mergeBackendConfigLayers(DEFAULT_BACKEND_CONFIG, loaded.global.rawBackends, loaded.project.rawBackends);
|
|
196
|
+
}
|
|
105
197
|
export function createSettingsDraftState(loaded, initialScope) {
|
|
106
198
|
const drafts = {
|
|
107
199
|
global: getScopeDisplayConfig(loaded, 'global'),
|
|
108
200
|
project: getScopeDisplayConfig(loaded, 'project')
|
|
109
201
|
};
|
|
202
|
+
const backendDrafts = {
|
|
203
|
+
global: getScopeDisplayBackends(loaded, 'global'),
|
|
204
|
+
project: getScopeDisplayBackends(loaded, 'project')
|
|
205
|
+
};
|
|
110
206
|
return {
|
|
111
207
|
scope: initialScope,
|
|
112
208
|
drafts,
|
|
113
|
-
|
|
209
|
+
backendDrafts,
|
|
210
|
+
config: clonePresentationConfig(drafts[initialScope]),
|
|
211
|
+
backends: cloneBackendConfig(backendDrafts[initialScope])
|
|
114
212
|
};
|
|
115
213
|
}
|
|
116
214
|
export function applySettingsValue(state, id, newValue) {
|
|
@@ -118,16 +216,23 @@ export function applySettingsValue(state, id, newValue) {
|
|
|
118
216
|
global: clonePresentationConfig(state.drafts.global),
|
|
119
217
|
project: clonePresentationConfig(state.drafts.project)
|
|
120
218
|
};
|
|
219
|
+
const nextBackendDrafts = {
|
|
220
|
+
global: cloneBackendConfig(state.backendDrafts.global),
|
|
221
|
+
project: cloneBackendConfig(state.backendDrafts.project)
|
|
222
|
+
};
|
|
121
223
|
let nextScope = state.scope;
|
|
122
224
|
if (id === 'scope' && (newValue === 'project' || newValue === 'global')) {
|
|
123
225
|
nextScope = newValue;
|
|
124
226
|
return {
|
|
125
227
|
scope: nextScope,
|
|
126
228
|
drafts: nextDrafts,
|
|
127
|
-
|
|
229
|
+
backendDrafts: nextBackendDrafts,
|
|
230
|
+
config: clonePresentationConfig(nextDrafts[nextScope]),
|
|
231
|
+
backends: cloneBackendConfig(nextBackendDrafts[nextScope])
|
|
128
232
|
};
|
|
129
233
|
}
|
|
130
234
|
const currentDraft = clonePresentationConfig(nextDrafts[nextScope]);
|
|
235
|
+
const currentBackends = cloneBackendConfig(nextBackendDrafts[nextScope]);
|
|
131
236
|
if (id === 'defaultMode' && (newValue === 'compact' || newValue === 'preview' || newValue === 'verbose')) {
|
|
132
237
|
currentDraft.defaultMode = newValue;
|
|
133
238
|
}
|
|
@@ -144,11 +249,58 @@ export function applySettingsValue(state, id, newValue) {
|
|
|
144
249
|
}
|
|
145
250
|
currentDraft.tools = nextTools;
|
|
146
251
|
}
|
|
252
|
+
if (id === 'backend:search:provider' && (newValue === 'duckduckgo' || newValue === 'searxng')) {
|
|
253
|
+
currentBackends.search.provider = newValue;
|
|
254
|
+
if (newValue === 'duckduckgo') {
|
|
255
|
+
delete currentBackends.search.fallback;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (id === 'backend:search:fallback') {
|
|
259
|
+
if (newValue === 'duckduckgo' && currentBackends.search.provider === 'searxng') {
|
|
260
|
+
currentBackends.search.fallback = 'duckduckgo';
|
|
261
|
+
}
|
|
262
|
+
else if (newValue === 'off' || currentBackends.search.provider !== 'searxng') {
|
|
263
|
+
delete currentBackends.search.fallback;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (id === 'backend:search:baseUrl') {
|
|
267
|
+
if (newValue.trim()) {
|
|
268
|
+
currentBackends.search.baseUrl = newValue.trim();
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
delete currentBackends.search.baseUrl;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (id === 'backend:fetch:provider' && (newValue === 'http' || newValue === 'firecrawl')) {
|
|
275
|
+
currentBackends.fetch.provider = newValue;
|
|
276
|
+
if (newValue === 'http') {
|
|
277
|
+
delete currentBackends.fetch.fallback;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (id === 'backend:fetch:fallback') {
|
|
281
|
+
if (newValue === 'http' && currentBackends.fetch.provider === 'firecrawl') {
|
|
282
|
+
currentBackends.fetch.fallback = 'http';
|
|
283
|
+
}
|
|
284
|
+
else if (newValue === 'off' || currentBackends.fetch.provider !== 'firecrawl') {
|
|
285
|
+
delete currentBackends.fetch.fallback;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (id === 'backend:fetch:baseUrl') {
|
|
289
|
+
if (newValue.trim()) {
|
|
290
|
+
currentBackends.fetch.baseUrl = newValue.trim();
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
delete currentBackends.fetch.baseUrl;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
147
296
|
nextDrafts[nextScope] = currentDraft;
|
|
297
|
+
nextBackendDrafts[nextScope] = currentBackends;
|
|
148
298
|
return {
|
|
149
299
|
scope: nextScope,
|
|
150
300
|
drafts: nextDrafts,
|
|
151
|
-
|
|
301
|
+
backendDrafts: nextBackendDrafts,
|
|
302
|
+
config: clonePresentationConfig(nextDrafts[nextScope]),
|
|
303
|
+
backends: cloneBackendConfig(nextBackendDrafts[nextScope])
|
|
152
304
|
};
|
|
153
305
|
}
|
|
154
306
|
export function collapsePresentationConfigToOverride(config, inheritedConfig) {
|
|
@@ -168,6 +320,44 @@ export function collapsePresentationConfigToOverride(config, inheritedConfig) {
|
|
|
168
320
|
tools
|
|
169
321
|
};
|
|
170
322
|
}
|
|
323
|
+
export function collapseBackendConfigToOverride(config, inheritedConfig) {
|
|
324
|
+
const override = {};
|
|
325
|
+
if (!sameJson(config.search, inheritedConfig.search)) {
|
|
326
|
+
override.search = config.search.provider !== inheritedConfig.search.provider
|
|
327
|
+
? { ...config.search }
|
|
328
|
+
: {
|
|
329
|
+
...(config.search.baseUrl !== inheritedConfig.search.baseUrl ? { baseUrl: config.search.baseUrl } : {}),
|
|
330
|
+
...(config.search.fallback !== inheritedConfig.search.fallback ? { fallback: config.search.fallback } : {}),
|
|
331
|
+
...(!sameJson(config.search.options, inheritedConfig.search.options) ? { options: config.search.options } : {})
|
|
332
|
+
};
|
|
333
|
+
if (config.search.provider !== inheritedConfig.search.provider) {
|
|
334
|
+
override.search.provider = config.search.provider;
|
|
335
|
+
}
|
|
336
|
+
else if (Object.keys(override.search).length === 0) {
|
|
337
|
+
delete override.search;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (!sameJson(config.fetch, inheritedConfig.fetch)) {
|
|
341
|
+
override.fetch = config.fetch.provider !== inheritedConfig.fetch.provider
|
|
342
|
+
? { ...config.fetch, apiKey: undefined }
|
|
343
|
+
: {
|
|
344
|
+
...(config.fetch.baseUrl !== inheritedConfig.fetch.baseUrl ? { baseUrl: config.fetch.baseUrl } : {}),
|
|
345
|
+
...(config.fetch.fallback !== inheritedConfig.fetch.fallback ? { fallback: config.fetch.fallback } : {}),
|
|
346
|
+
...(!sameJson(config.fetch.options, inheritedConfig.fetch.options) ? { options: config.fetch.options } : {})
|
|
347
|
+
};
|
|
348
|
+
delete override.fetch.apiKey;
|
|
349
|
+
if (config.fetch.provider !== inheritedConfig.fetch.provider) {
|
|
350
|
+
override.fetch.provider = config.fetch.provider;
|
|
351
|
+
}
|
|
352
|
+
else if (Object.keys(override.fetch).length === 0) {
|
|
353
|
+
delete override.fetch;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (!sameJson(config.headless, inheritedConfig.headless)) {
|
|
357
|
+
override.headless = { ...config.headless };
|
|
358
|
+
}
|
|
359
|
+
return override;
|
|
360
|
+
}
|
|
171
361
|
export function handleSettingsShortcut(data) {
|
|
172
362
|
if (data === '\u001b') {
|
|
173
363
|
return { action: 'cancel' };
|
|
@@ -184,9 +374,10 @@ async function openActionMenu(ctx) {
|
|
|
184
374
|
return ctx.ui.custom((tui, theme, _kb, done) => {
|
|
185
375
|
const container = new Container();
|
|
186
376
|
const items = [
|
|
187
|
-
{ value: 'settings', label: 'Settings', description: 'Edit presentation modes' },
|
|
377
|
+
{ value: 'settings', label: 'Settings', description: 'Edit presentation modes and backends' },
|
|
188
378
|
{ value: 'show', label: 'Show config', description: 'Print effective config paths and modes' },
|
|
189
379
|
{ value: 'doctor', label: 'Doctor', description: 'Check runtime dependencies and browser detection' },
|
|
380
|
+
{ value: 'changelog', label: 'Changelog', description: 'Show latest package changelog' },
|
|
190
381
|
{ value: 'reset-project', label: 'Reset project config', description: 'Delete project-level overrides' },
|
|
191
382
|
{ value: 'reset-global', label: 'Reset global config', description: 'Delete global overrides' }
|
|
192
383
|
];
|
|
@@ -214,22 +405,120 @@ async function openActionMenu(ctx) {
|
|
|
214
405
|
};
|
|
215
406
|
});
|
|
216
407
|
}
|
|
217
|
-
async function
|
|
408
|
+
async function openSettingsSectionMenu(ctx) {
|
|
409
|
+
return ctx.ui.custom((tui, theme, _kb, done) => {
|
|
410
|
+
const container = new Container();
|
|
411
|
+
const items = [
|
|
412
|
+
{ value: 'presentation', label: 'Presentation', description: 'Compact, preview, and verbose output modes' },
|
|
413
|
+
{ value: 'backends', label: 'Backends', description: 'Search/fetch providers, URLs, and fallbacks' }
|
|
414
|
+
];
|
|
415
|
+
container.addChild(new DynamicBorder((text) => theme.fg('accent', text)));
|
|
416
|
+
container.addChild(new Text(theme.fg('accent', theme.bold('pi-web-agent settings')), 1, 0));
|
|
417
|
+
const list = new SelectList(items, items.length, {
|
|
418
|
+
selectedPrefix: (text) => theme.fg('accent', text),
|
|
419
|
+
selectedText: (text) => theme.fg('accent', text),
|
|
420
|
+
description: (text) => theme.fg('muted', text),
|
|
421
|
+
scrollInfo: (text) => theme.fg('dim', text),
|
|
422
|
+
noMatch: (text) => theme.fg('warning', text)
|
|
423
|
+
});
|
|
424
|
+
list.onSelect = (item) => done(item.value);
|
|
425
|
+
list.onCancel = () => done(undefined);
|
|
426
|
+
container.addChild(list);
|
|
427
|
+
container.addChild(new Text(theme.fg('dim', '↑↓ navigate • enter select • esc cancel'), 1, 0));
|
|
428
|
+
container.addChild(new DynamicBorder((text) => theme.fg('accent', text)));
|
|
429
|
+
return {
|
|
430
|
+
render: (width) => container.render(width),
|
|
431
|
+
invalidate: () => container.invalidate(),
|
|
432
|
+
handleInput: (data) => {
|
|
433
|
+
list.handleInput?.(data);
|
|
434
|
+
tui.requestRender?.();
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
async function openPresentationSettingsUi(ctx, loaded, initialScope) {
|
|
218
440
|
return ctx.ui.custom((_tui, theme, _kb, done) => {
|
|
219
441
|
let state = createSettingsDraftState(loaded, initialScope);
|
|
220
442
|
let settingsList;
|
|
221
443
|
const container = new Container();
|
|
222
|
-
container.addChild(new Text(theme.fg('accent', theme.bold('pi-web-agent
|
|
444
|
+
container.addChild(new Text(theme.fg('accent', theme.bold('pi-web-agent · presentation')), 1, 1));
|
|
223
445
|
container.addChild(new Text(theme.fg('muted', 'Ctrl+S save · Ctrl+R reset scope · Esc cancel'), 1, 2));
|
|
224
446
|
const rebuildSettingsList = () => {
|
|
225
447
|
if (settingsList) {
|
|
226
448
|
container.removeChild(settingsList);
|
|
227
449
|
}
|
|
228
|
-
settingsList = new SettingsList(
|
|
450
|
+
settingsList = new SettingsList(buildPresentationSettingsItems(state.scope, state.config), Math.min(PRESENTATION_TOOL_NAMES.length + 8, 18), getSettingsListTheme(), (id, newValue) => {
|
|
451
|
+
state = applySettingsValue(state, id, newValue);
|
|
452
|
+
rebuildSettingsList();
|
|
453
|
+
container.invalidate();
|
|
454
|
+
}, () => done({ action: 'save', scope: state.scope, config: state.config, backends: state.backends }), { enableSearch: true });
|
|
455
|
+
container.addChild(settingsList);
|
|
456
|
+
};
|
|
457
|
+
rebuildSettingsList();
|
|
458
|
+
return {
|
|
459
|
+
render: (width) => container.render(width),
|
|
460
|
+
invalidate: () => container.invalidate(),
|
|
461
|
+
handleInput: (data) => {
|
|
462
|
+
const shortcut = handleSettingsShortcut(JSON.stringify(data).slice(1, -1));
|
|
463
|
+
if (shortcut?.action === 'cancel') {
|
|
464
|
+
done({ action: 'cancel' });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (shortcut?.action === 'reset') {
|
|
468
|
+
done({ action: 'reset', scope: state.scope });
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (shortcut?.action === 'save') {
|
|
472
|
+
done({ action: 'save', scope: state.scope, config: state.config, backends: state.backends });
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
settingsList.handleInput?.(data);
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
async function openBackendSettingsUi(ctx, loaded, initialScope) {
|
|
481
|
+
return ctx.ui.custom((tui, theme, _kb, done) => {
|
|
482
|
+
let state = createSettingsDraftState(loaded, initialScope);
|
|
483
|
+
let settingsList;
|
|
484
|
+
const container = new Container();
|
|
485
|
+
container.addChild(new Text(theme.fg('accent', theme.bold('pi-web-agent · backends')), 1, 1));
|
|
486
|
+
container.addChild(new Text(theme.fg('muted', 'Ctrl+S save · Ctrl+R reset scope · Esc cancel · API keys stay in env vars'), 1, 2));
|
|
487
|
+
const editUrl = async (id) => {
|
|
488
|
+
const isSearchUrl = id === 'backend:search:baseUrl';
|
|
489
|
+
const label = isSearchUrl ? 'SearXNG base URL' : 'Firecrawl base URL';
|
|
490
|
+
const currentValue = isSearchUrl ? state.backends.search.baseUrl : state.backends.fetch.baseUrl;
|
|
491
|
+
const entered = await ctx.ui.input(label, currentValue ?? (isSearchUrl ? 'http://localhost:8080' : 'http://localhost:3002'));
|
|
492
|
+
if (entered === undefined)
|
|
493
|
+
return;
|
|
494
|
+
if (!entered.trim()) {
|
|
495
|
+
state = applySettingsValue(state, id, '');
|
|
496
|
+
rebuildSettingsList();
|
|
497
|
+
tui.requestRender?.();
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const validated = validateBackendUrl(entered);
|
|
501
|
+
if (!validated.ok) {
|
|
502
|
+
ctx.ui.notify(validated.message, 'warning');
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
state = applySettingsValue(state, id, validated.value);
|
|
506
|
+
rebuildSettingsList();
|
|
507
|
+
tui.requestRender?.();
|
|
508
|
+
};
|
|
509
|
+
const rebuildSettingsList = () => {
|
|
510
|
+
if (settingsList) {
|
|
511
|
+
container.removeChild(settingsList);
|
|
512
|
+
}
|
|
513
|
+
settingsList = new SettingsList(buildBackendSettingsItems(state.scope, state.backends), 12, getSettingsListTheme(), (id, newValue) => {
|
|
514
|
+
if (id === 'backend:search:baseUrl' || id === 'backend:fetch:baseUrl') {
|
|
515
|
+
void editUrl(id);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
229
518
|
state = applySettingsValue(state, id, newValue);
|
|
230
519
|
rebuildSettingsList();
|
|
231
520
|
container.invalidate();
|
|
232
|
-
}, () => done({ action: 'save', scope: state.scope, config: state.config }), { enableSearch: true });
|
|
521
|
+
}, () => done({ action: 'save', scope: state.scope, config: state.config, backends: state.backends }), { enableSearch: true });
|
|
233
522
|
container.addChild(settingsList);
|
|
234
523
|
};
|
|
235
524
|
rebuildSettingsList();
|
|
@@ -247,7 +536,7 @@ async function openSettingsUi(ctx, loaded, initialScope) {
|
|
|
247
536
|
return;
|
|
248
537
|
}
|
|
249
538
|
if (shortcut?.action === 'save') {
|
|
250
|
-
done({ action: 'save', scope: state.scope, config: state.config });
|
|
539
|
+
done({ action: 'save', scope: state.scope, config: state.config, backends: state.backends });
|
|
251
540
|
return;
|
|
252
541
|
}
|
|
253
542
|
settingsList.handleInput?.(data);
|
|
@@ -258,6 +547,7 @@ async function openSettingsUi(ctx, loaded, initialScope) {
|
|
|
258
547
|
export function registerWebAgentConfigCommands(pi, deps = {}) {
|
|
259
548
|
const load = deps.load ?? (() => loadPresentationConfigLayers());
|
|
260
549
|
const save = deps.save ?? ((scope, config) => savePresentationConfigScope({}, scope, config));
|
|
550
|
+
const saveBackends = deps.saveBackends ?? ((scope, config) => saveBackendConfigScope({}, scope, config));
|
|
261
551
|
const reset = deps.reset ?? ((scope) => resetPresentationConfigScope({}, scope));
|
|
262
552
|
const resolveBrowser = deps.resolveBrowser ?? (() => resolveBrowserExecutable({}));
|
|
263
553
|
const runtime = deps.runtime ?? {
|
|
@@ -362,7 +652,12 @@ export function registerWebAgentConfigCommands(pi, deps = {}) {
|
|
|
362
652
|
if (!action || action === 'settings') {
|
|
363
653
|
const loaded = await load();
|
|
364
654
|
const initialScope = 'project';
|
|
365
|
-
const
|
|
655
|
+
const section = await openSettingsSectionMenu(ctx);
|
|
656
|
+
if (!section)
|
|
657
|
+
return;
|
|
658
|
+
const result = section === 'presentation'
|
|
659
|
+
? await openPresentationSettingsUi(ctx, loaded, initialScope)
|
|
660
|
+
: await openBackendSettingsUi(ctx, loaded, initialScope);
|
|
366
661
|
if (!result || result.action === 'cancel') {
|
|
367
662
|
return;
|
|
368
663
|
}
|
|
@@ -371,8 +666,13 @@ export function registerWebAgentConfigCommands(pi, deps = {}) {
|
|
|
371
666
|
ctx.ui.notify(`Reset ${result.scope} config`, 'info');
|
|
372
667
|
return;
|
|
373
668
|
}
|
|
374
|
-
|
|
375
|
-
|
|
669
|
+
if (section === 'presentation') {
|
|
670
|
+
await save(result.scope, collapsePresentationConfigToOverride(result.config, getInheritedConfigForScope(loaded, result.scope)));
|
|
671
|
+
ctx.ui.notify(`Saved ${result.scope} presentation config`, 'info');
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
await saveBackends(result.scope, collapseBackendConfigToOverride(result.backends, getInheritedBackendsForScope(loaded, result.scope)));
|
|
675
|
+
ctx.ui.notify(`Saved ${result.scope} backend config`, 'info');
|
|
376
676
|
return;
|
|
377
677
|
}
|
|
378
678
|
ctx.ui.notify('Use /web-agent, /web-agent show, /web-agent doctor, /web-agent changelog, /web-agent reset project, or /web-agent settings', 'info');
|
|
@@ -6,7 +6,8 @@ export declare function headlessFetch(url: string, { configuredPath, resolveBrow
|
|
|
6
6
|
configuredPath?: string;
|
|
7
7
|
}) => Promise<BrowserResolutionResult>;
|
|
8
8
|
launchBrowser?: (options: {
|
|
9
|
-
executablePath
|
|
9
|
+
executablePath?: string;
|
|
10
|
+
headless: true;
|
|
10
11
|
}) => Promise<{
|
|
11
12
|
newContext: () => Promise<{
|
|
12
13
|
newPage: () => Promise<any>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chromium } from 'playwright
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
2
|
import { extractReadableContentSafely } from '../extract/readability.js';
|
|
3
3
|
import { resolveBrowserExecutable } from './browser-resolution.js';
|
|
4
4
|
function cleanupRenderedText(text) {
|
|
@@ -7,9 +7,9 @@ function cleanupRenderedText(text) {
|
|
|
7
7
|
cleaned = cleaned.replace(/\s+/g, ' ').trim();
|
|
8
8
|
return cleaned;
|
|
9
9
|
}
|
|
10
|
-
export async function headlessFetch(url, { configuredPath, resolveBrowser = (options) => resolveBrowserExecutable({ configuredPath: options?.configuredPath }), launchBrowser = ({ executablePath }) => chromium.launch({ executablePath, headless:
|
|
10
|
+
export async function headlessFetch(url, { configuredPath, resolveBrowser = (options) => resolveBrowserExecutable({ configuredPath: options?.configuredPath }), launchBrowser = ({ executablePath, headless }) => chromium.launch(executablePath ? { executablePath, headless } : { headless }), now = () => Date.now() } = {}) {
|
|
11
11
|
const resolved = await resolveBrowser({ configuredPath });
|
|
12
|
-
if (!resolved.ok) {
|
|
12
|
+
if (!resolved.ok && resolved.error.code === 'CONFIGURED_BROWSER_NOT_FOUND') {
|
|
13
13
|
return {
|
|
14
14
|
status: 'error',
|
|
15
15
|
url,
|
|
@@ -17,11 +17,15 @@ export async function headlessFetch(url, { configuredPath, resolveBrowser = (opt
|
|
|
17
17
|
error: resolved.error
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
+
const browserName = resolved.ok ? resolved.browser : 'chromium';
|
|
21
|
+
const launchOptions = resolved.ok
|
|
22
|
+
? { executablePath: resolved.executablePath, headless: true }
|
|
23
|
+
: { headless: true };
|
|
20
24
|
let browser;
|
|
21
25
|
let context;
|
|
22
26
|
let page;
|
|
23
27
|
try {
|
|
24
|
-
browser = await launchBrowser(
|
|
28
|
+
browser = await launchBrowser(launchOptions);
|
|
25
29
|
context = await browser.newContext();
|
|
26
30
|
page = await context.newPage();
|
|
27
31
|
const startedAt = now();
|
|
@@ -42,7 +46,7 @@ export async function headlessFetch(url, { configuredPath, resolveBrowser = (opt
|
|
|
42
46
|
metadata: {
|
|
43
47
|
method: 'headless',
|
|
44
48
|
cacheHit: false,
|
|
45
|
-
browser:
|
|
49
|
+
browser: browserName,
|
|
46
50
|
navigationMs: finishedAt - startedAt
|
|
47
51
|
},
|
|
48
52
|
error: {
|
|
@@ -58,7 +62,7 @@ export async function headlessFetch(url, { configuredPath, resolveBrowser = (opt
|
|
|
58
62
|
metadata: {
|
|
59
63
|
method: 'headless',
|
|
60
64
|
cacheHit: false,
|
|
61
|
-
browser:
|
|
65
|
+
browser: browserName,
|
|
62
66
|
navigationMs: finishedAt - startedAt,
|
|
63
67
|
truncated: cleanedContent.text.length >= 4000
|
|
64
68
|
}
|
|
@@ -71,7 +75,7 @@ export async function headlessFetch(url, { configuredPath, resolveBrowser = (opt
|
|
|
71
75
|
metadata: {
|
|
72
76
|
method: 'headless',
|
|
73
77
|
cacheHit: false,
|
|
74
|
-
browser:
|
|
78
|
+
browser: browserName
|
|
75
79
|
},
|
|
76
80
|
error: {
|
|
77
81
|
code: 'HEADLESS_NAVIGATION_FAILED',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SearchResult } from '../types.js';
|
|
2
|
-
export declare function selectCandidates({ results, seenUrls, maxCandidates }: {
|
|
2
|
+
export declare function selectCandidates({ query, results, seenUrls, maxCandidates }: {
|
|
3
|
+
query?: string;
|
|
3
4
|
results: SearchResult[];
|
|
4
5
|
seenUrls: Set<string>;
|
|
5
6
|
maxCandidates: number;
|
|
@@ -1,16 +1,35 @@
|
|
|
1
|
-
|
|
1
|
+
import { classifySourceProfile } from './source-profile.js';
|
|
2
|
+
function wantsDiscussionSources(query = '') {
|
|
3
|
+
return /reddit|forum|forums|discussion|thread|comments|community|user experience|people recommend/i.test(query);
|
|
4
|
+
}
|
|
5
|
+
function candidateScore(result, query) {
|
|
2
6
|
const url = result.url.toLowerCase();
|
|
3
|
-
|
|
7
|
+
const profile = classifySourceProfile(result.url);
|
|
8
|
+
const wantsThreads = wantsDiscussionSources(query);
|
|
9
|
+
if (profile.kind === 'official-docs')
|
|
4
10
|
return 0;
|
|
5
|
-
if (
|
|
11
|
+
if (profile.kind === 'official-api')
|
|
6
12
|
return 1;
|
|
7
|
-
if (
|
|
13
|
+
if (wantsThreads) {
|
|
14
|
+
if (profile.kind === 'forum-thread')
|
|
15
|
+
return 2;
|
|
16
|
+
if (profile.kind === 'issue-thread')
|
|
17
|
+
return 3;
|
|
18
|
+
if (url.includes('github.com/'))
|
|
19
|
+
return 4;
|
|
20
|
+
if (profile.kind === 'package-page')
|
|
21
|
+
return 6;
|
|
22
|
+
return 5;
|
|
23
|
+
}
|
|
24
|
+
if (profile.kind === 'issue-thread')
|
|
8
25
|
return 2;
|
|
9
|
-
if (url.includes('
|
|
10
|
-
return
|
|
11
|
-
|
|
26
|
+
if (url.includes('github.com/'))
|
|
27
|
+
return 3;
|
|
28
|
+
if (profile.kind === 'package-page')
|
|
29
|
+
return 5;
|
|
30
|
+
return 4;
|
|
12
31
|
}
|
|
13
|
-
export function selectCandidates({ results, seenUrls, maxCandidates }) {
|
|
32
|
+
export function selectCandidates({ query, results, seenUrls, maxCandidates }) {
|
|
14
33
|
const deduped = new Map();
|
|
15
34
|
for (const result of results) {
|
|
16
35
|
if (seenUrls.has(result.url))
|
|
@@ -19,6 +38,6 @@ export function selectCandidates({ results, seenUrls, maxCandidates }) {
|
|
|
19
38
|
deduped.set(result.url, result);
|
|
20
39
|
}
|
|
21
40
|
return [...deduped.values()]
|
|
22
|
-
.sort((left, right) => candidateScore(left) - candidateScore(right))
|
|
41
|
+
.sort((left, right) => candidateScore(left, query) - candidateScore(right, query))
|
|
23
42
|
.slice(0, maxCandidates);
|
|
24
43
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function extractDirectUrls(query: string): string[];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const TRACKING_PARAMS = new Set([
|
|
2
|
+
'utm_source',
|
|
3
|
+
'utm_medium',
|
|
4
|
+
'utm_campaign',
|
|
5
|
+
'utm_term',
|
|
6
|
+
'utm_content',
|
|
7
|
+
'utm_name',
|
|
8
|
+
'fbclid',
|
|
9
|
+
'gclid'
|
|
10
|
+
]);
|
|
11
|
+
function stripTrailingPunctuation(raw) {
|
|
12
|
+
let next = raw.trim();
|
|
13
|
+
while (/[),.;!?\]]$/.test(next)) {
|
|
14
|
+
const last = next.at(-1);
|
|
15
|
+
if (last === ')' && next.includes('(') && next.lastIndexOf('(') > next.lastIndexOf(')'))
|
|
16
|
+
break;
|
|
17
|
+
next = next.slice(0, -1);
|
|
18
|
+
}
|
|
19
|
+
return next;
|
|
20
|
+
}
|
|
21
|
+
function normalizeDirectUrl(raw) {
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(stripTrailingPunctuation(raw));
|
|
24
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
25
|
+
return undefined;
|
|
26
|
+
for (const key of [...url.searchParams.keys()]) {
|
|
27
|
+
if (TRACKING_PARAMS.has(key.toLowerCase())) {
|
|
28
|
+
url.searchParams.delete(key);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
url.hash = '';
|
|
32
|
+
return url.toString().replace(/\/$/, '');
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function extractDirectUrls(query) {
|
|
39
|
+
const matches = query.match(/https?:\/\/\S+/gi) ?? [];
|
|
40
|
+
const urls = new Set();
|
|
41
|
+
for (const match of matches) {
|
|
42
|
+
const normalized = normalizeDirectUrl(match);
|
|
43
|
+
if (normalized)
|
|
44
|
+
urls.add(normalized);
|
|
45
|
+
}
|
|
46
|
+
return [...urls];
|
|
47
|
+
}
|
|
@@ -7,5 +7,9 @@ export function createResearchWorkflow({ backendConfig, search, fetchPage, headl
|
|
|
7
7
|
const resolvedFetchPage = fetchPage ?? backends.fetchPage;
|
|
8
8
|
const resolvedHeadlessFetch = headlessFetch ?? backends.headlessFetch;
|
|
9
9
|
const worker = createResearchWorker({ search: resolvedSearch, fetchPage: resolvedFetchPage });
|
|
10
|
-
return createResearchOrchestrator({
|
|
10
|
+
return createResearchOrchestrator({
|
|
11
|
+
worker,
|
|
12
|
+
fetchDirect: resolvedFetchPage,
|
|
13
|
+
headlessFetch: resolvedHeadlessFetch
|
|
14
|
+
});
|
|
11
15
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { WebFetchHeadlessResponse } from '../types.js';
|
|
1
|
+
import type { WebFetchHeadlessResponse, WebFetchResponse } from '../types.js';
|
|
2
2
|
import type { ResearchEvidence, ResearchOrchestratorDecision, ResearchWorkerResult } from './research-types.js';
|
|
3
|
-
export declare function createResearchOrchestrator({ worker, headlessFetch }: {
|
|
3
|
+
export declare function createResearchOrchestrator({ worker, fetchDirect, headlessFetch }: {
|
|
4
4
|
worker: {
|
|
5
5
|
run: (input: {
|
|
6
6
|
query: string;
|
|
@@ -8,6 +8,9 @@ export declare function createResearchOrchestrator({ worker, headlessFetch }: {
|
|
|
8
8
|
maxFetches: number;
|
|
9
9
|
}) => Promise<ResearchWorkerResult>;
|
|
10
10
|
};
|
|
11
|
+
fetchDirect?: (input: {
|
|
12
|
+
url: string;
|
|
13
|
+
}) => Promise<WebFetchResponse>;
|
|
11
14
|
headlessFetch: (input: {
|
|
12
15
|
url: string;
|
|
13
16
|
}) => Promise<WebFetchHeadlessResponse>;
|
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
import { rankEvidence } from './evidence-ranker.js';
|
|
2
2
|
import { planSearchQueries } from './query-planner.js';
|
|
3
|
+
import { classifySourceProfile } from './source-profile.js';
|
|
4
|
+
import { extractDirectUrls } from './direct-url.js';
|
|
3
5
|
import { decideNextResearchStep } from './stop-decider.js';
|
|
4
6
|
const DEFAULT_MAX_PASSES = 3;
|
|
5
7
|
const DEFAULT_MAX_FETCHES_PER_PASS = 4;
|
|
6
8
|
const DEFAULT_MAX_HEADLESS_ATTEMPTS = 2;
|
|
7
9
|
function classifyEvidenceUrl(url) {
|
|
8
|
-
|
|
9
|
-
return 'official-api';
|
|
10
|
-
if (url.includes('playwright.dev/docs') || url.includes('vitest.dev/guide/'))
|
|
11
|
-
return 'official-docs';
|
|
12
|
-
if (url.includes('github.com/vitest-dev/vitest') && url.includes('/docs/'))
|
|
13
|
-
return 'official-docs';
|
|
14
|
-
if (url.includes('learn.microsoft.com'))
|
|
15
|
-
return 'official-docs';
|
|
16
|
-
if (url.includes('github.com/') && url.includes('/issues/'))
|
|
17
|
-
return 'issue-thread';
|
|
18
|
-
if (url.includes('npmjs.com/package/'))
|
|
19
|
-
return 'package-page';
|
|
20
|
-
return 'community';
|
|
10
|
+
return classifySourceProfile(url).sourceKind;
|
|
21
11
|
}
|
|
22
12
|
function summarizeText(text, maxLength = 180) {
|
|
23
13
|
return text.replace(/\s+/g, ' ').trim().slice(0, maxLength);
|
|
@@ -25,6 +15,20 @@ function summarizeText(text, maxLength = 180) {
|
|
|
25
15
|
function isBotCheckContent({ title = '', text }) {
|
|
26
16
|
return /performing security verification|security service|verify you are not a bot|just a moment|checking your browser/i.test(`${title}\n${text}`);
|
|
27
17
|
}
|
|
18
|
+
function evidenceFromFetch(result) {
|
|
19
|
+
if (result.status !== 'ok' || !result.content?.text.trim())
|
|
20
|
+
return null;
|
|
21
|
+
if (isBotCheckContent({ title: result.content.title, text: result.content.text }))
|
|
22
|
+
return null;
|
|
23
|
+
return {
|
|
24
|
+
title: result.content.title ?? result.url,
|
|
25
|
+
url: result.url,
|
|
26
|
+
sourceKind: classifyEvidenceUrl(result.url),
|
|
27
|
+
method: result.metadata.method,
|
|
28
|
+
summary: summarizeText(result.content.text),
|
|
29
|
+
supports: [summarizeText(result.content.text, 120)]
|
|
30
|
+
};
|
|
31
|
+
}
|
|
28
32
|
function evidenceFromHeadless(result) {
|
|
29
33
|
if (result.status !== 'ok' || !result.content?.text.trim())
|
|
30
34
|
return null;
|
|
@@ -39,15 +43,28 @@ function evidenceFromHeadless(result) {
|
|
|
39
43
|
supports: [summarizeText(result.content.text, 120)]
|
|
40
44
|
};
|
|
41
45
|
}
|
|
42
|
-
function
|
|
46
|
+
function combinedWorkerPass({ lastPass, previousQueries, allGaps, allLowValueOutcomes, exhaustedBudget }) {
|
|
43
47
|
return {
|
|
44
|
-
searchQueries: previousQueries,
|
|
45
|
-
evidence: [],
|
|
48
|
+
searchQueries: lastPass?.searchQueries ?? previousQueries,
|
|
49
|
+
evidence: lastPass?.evidence ?? [],
|
|
46
50
|
gaps: allGaps,
|
|
47
51
|
lowValueOutcomes: allLowValueOutcomes,
|
|
52
|
+
suggestedHeadlessUrl: lastPass?.suggestedHeadlessUrl,
|
|
48
53
|
exhaustedBudget
|
|
49
54
|
};
|
|
50
55
|
}
|
|
56
|
+
function directUnreadableMessage(url) {
|
|
57
|
+
return classifySourceProfile(url).kind === 'forum-thread'
|
|
58
|
+
? `Thread source could not be read reliably: ${url}`
|
|
59
|
+
: `Direct URL could not be read reliably: ${url}`;
|
|
60
|
+
}
|
|
61
|
+
function shouldRetryDirectWithHeadless(result, evidence) {
|
|
62
|
+
if (result.status === 'needs_headless')
|
|
63
|
+
return true;
|
|
64
|
+
if (result.status !== 'ok' || evidence)
|
|
65
|
+
return false;
|
|
66
|
+
return classifySourceProfile(result.url).shouldPreferHeadlessWhenWeak;
|
|
67
|
+
}
|
|
51
68
|
function buildMetadata({ previousQueries, allEvidence, allGaps, allLowValueOutcomes, headlessAttempts, exhaustedBudget }) {
|
|
52
69
|
return {
|
|
53
70
|
searchPasses: previousQueries.length,
|
|
@@ -70,7 +87,7 @@ function decisionForAnswer(action, query, ranked) {
|
|
|
70
87
|
followupQuery: query
|
|
71
88
|
};
|
|
72
89
|
}
|
|
73
|
-
export function createResearchOrchestrator({ worker, headlessFetch }) {
|
|
90
|
+
export function createResearchOrchestrator({ worker, fetchDirect, headlessFetch }) {
|
|
74
91
|
return {
|
|
75
92
|
async run({ query }) {
|
|
76
93
|
const allEvidence = [];
|
|
@@ -80,6 +97,41 @@ export function createResearchOrchestrator({ worker, headlessFetch }) {
|
|
|
80
97
|
const suggestedHeadlessUrls = [];
|
|
81
98
|
let headlessAttempts = 0;
|
|
82
99
|
let lastPass;
|
|
100
|
+
if (fetchDirect) {
|
|
101
|
+
for (const url of extractDirectUrls(query).slice(0, 3)) {
|
|
102
|
+
const directResult = await fetchDirect({ url });
|
|
103
|
+
const directEvidence = evidenceFromFetch(directResult);
|
|
104
|
+
if (directEvidence) {
|
|
105
|
+
allEvidence.push(directEvidence);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (shouldRetryDirectWithHeadless(directResult, directEvidence)) {
|
|
109
|
+
if (headlessAttempts < DEFAULT_MAX_HEADLESS_ATTEMPTS) {
|
|
110
|
+
headlessAttempts++;
|
|
111
|
+
const headlessResult = await headlessFetch({ url: directResult.url });
|
|
112
|
+
const headlessEvidence = evidenceFromHeadless(headlessResult);
|
|
113
|
+
if (headlessEvidence) {
|
|
114
|
+
allEvidence.push(headlessEvidence);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
allGaps.push({ kind: 'fetch-failed', message: directUnreadableMessage(directResult.url) });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
allGaps.push({ kind: 'fetch-failed', message: directUnreadableMessage(directResult.url) });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else if (directResult.status !== 'ok') {
|
|
125
|
+
allGaps.push({
|
|
126
|
+
kind: 'fetch-failed',
|
|
127
|
+
message: directResult.error?.message ?? `Direct URL fetch failed for ${directResult.url}`
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
allGaps.push({ kind: 'fetch-failed', message: directUnreadableMessage(directResult.url) });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
83
135
|
for (let passIndex = 0; passIndex < DEFAULT_MAX_PASSES; passIndex++) {
|
|
84
136
|
const queries = planSearchQueries({
|
|
85
137
|
originalQuery: query,
|
|
@@ -127,7 +179,13 @@ export function createResearchOrchestrator({ worker, headlessFetch }) {
|
|
|
127
179
|
return {
|
|
128
180
|
decision: decisionForAnswer(updatedDecision.action === 'answer' ? 'answer' : 'answer-with-caveat', query, updatedRanked),
|
|
129
181
|
evidence: updatedRanked,
|
|
130
|
-
workerPass:
|
|
182
|
+
workerPass: combinedWorkerPass({
|
|
183
|
+
lastPass,
|
|
184
|
+
previousQueries,
|
|
185
|
+
allGaps,
|
|
186
|
+
allLowValueOutcomes,
|
|
187
|
+
exhaustedBudget: updatedDecision.action !== 'answer'
|
|
188
|
+
}),
|
|
131
189
|
metadata: buildMetadata({
|
|
132
190
|
previousQueries,
|
|
133
191
|
allEvidence,
|
|
@@ -146,7 +204,13 @@ export function createResearchOrchestrator({ worker, headlessFetch }) {
|
|
|
146
204
|
approvedEvidence: ranked
|
|
147
205
|
},
|
|
148
206
|
evidence: ranked,
|
|
149
|
-
workerPass:
|
|
207
|
+
workerPass: combinedWorkerPass({
|
|
208
|
+
lastPass,
|
|
209
|
+
previousQueries,
|
|
210
|
+
allGaps,
|
|
211
|
+
allLowValueOutcomes,
|
|
212
|
+
exhaustedBudget: false
|
|
213
|
+
}),
|
|
150
214
|
metadata: buildMetadata({
|
|
151
215
|
previousQueries,
|
|
152
216
|
allEvidence,
|
|
@@ -161,7 +225,13 @@ export function createResearchOrchestrator({ worker, headlessFetch }) {
|
|
|
161
225
|
return {
|
|
162
226
|
decision: decisionForAnswer(decision.action, query, ranked),
|
|
163
227
|
evidence: ranked,
|
|
164
|
-
workerPass:
|
|
228
|
+
workerPass: combinedWorkerPass({
|
|
229
|
+
lastPass,
|
|
230
|
+
previousQueries,
|
|
231
|
+
allGaps,
|
|
232
|
+
allLowValueOutcomes,
|
|
233
|
+
exhaustedBudget: decision.action === 'answer-with-caveat'
|
|
234
|
+
}),
|
|
165
235
|
metadata: buildMetadata({
|
|
166
236
|
previousQueries,
|
|
167
237
|
allEvidence,
|
|
@@ -178,13 +248,13 @@ export function createResearchOrchestrator({ worker, headlessFetch }) {
|
|
|
178
248
|
return {
|
|
179
249
|
decision: decisionForAnswer('answer-with-caveat', query, ranked),
|
|
180
250
|
evidence: ranked,
|
|
181
|
-
workerPass:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
251
|
+
workerPass: combinedWorkerPass({
|
|
252
|
+
lastPass,
|
|
253
|
+
previousQueries,
|
|
254
|
+
allGaps,
|
|
255
|
+
allLowValueOutcomes,
|
|
256
|
+
exhaustedBudget: true
|
|
257
|
+
}),
|
|
188
258
|
metadata: buildMetadata({
|
|
189
259
|
previousQueries,
|
|
190
260
|
allEvidence,
|
|
@@ -1,18 +1,7 @@
|
|
|
1
1
|
import { selectCandidates } from './candidate-selector.js';
|
|
2
|
+
import { classifySourceProfile } from './source-profile.js';
|
|
2
3
|
function classifySource(url) {
|
|
3
|
-
|
|
4
|
-
return 'official-api';
|
|
5
|
-
if (url.includes('playwright.dev/docs') || url.includes('vitest.dev/guide/'))
|
|
6
|
-
return 'official-docs';
|
|
7
|
-
if (url.includes('github.com/vitest-dev/vitest') && url.includes('/docs/'))
|
|
8
|
-
return 'official-docs';
|
|
9
|
-
if (url.includes('learn.microsoft.com'))
|
|
10
|
-
return 'official-docs';
|
|
11
|
-
if (url.includes('github.com/') && url.includes('/issues/'))
|
|
12
|
-
return 'issue-thread';
|
|
13
|
-
if (url.includes('npmjs.com/package/'))
|
|
14
|
-
return 'package-page';
|
|
15
|
-
return 'community';
|
|
4
|
+
return classifySourceProfile(url).sourceKind;
|
|
16
5
|
}
|
|
17
6
|
function summarizeText(text, maxLength = 180) {
|
|
18
7
|
return text.replace(/\s+/g, ' ').trim().slice(0, maxLength);
|
|
@@ -95,6 +84,7 @@ export function createResearchWorker({ search, fetchPage }) {
|
|
|
95
84
|
};
|
|
96
85
|
}
|
|
97
86
|
const candidates = selectCandidates({
|
|
87
|
+
query,
|
|
98
88
|
results: searchResult.results,
|
|
99
89
|
seenUrls: new Set(evidence.map((item) => item.url)),
|
|
100
90
|
maxCandidates: maxFetches
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ResearchSourceKind } from './research-types.js';
|
|
2
|
+
export type SourceProfileKind = 'official-docs' | 'official-api' | 'issue-thread' | 'forum-thread' | 'package-page' | 'community';
|
|
3
|
+
export type SourceProfile = {
|
|
4
|
+
kind: SourceProfileKind;
|
|
5
|
+
sourceKind: ResearchSourceKind;
|
|
6
|
+
shouldPreferHeadlessWhenWeak: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare function classifySourceProfile(rawUrl: string): SourceProfile;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const COMMUNITY_FORUM_HOST_RE = /(^|\.)(community|forum|forums|discuss|discourse)\./;
|
|
2
|
+
function profile(kind, sourceKind, shouldPreferHeadlessWhenWeak) {
|
|
3
|
+
return { kind, sourceKind, shouldPreferHeadlessWhenWeak };
|
|
4
|
+
}
|
|
5
|
+
function parseUrl(rawUrl) {
|
|
6
|
+
try {
|
|
7
|
+
return new URL(rawUrl);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function isOfficialApi(host, path) {
|
|
14
|
+
return ((host === 'playwright.dev' && path.startsWith('/docs/api/')) ||
|
|
15
|
+
(host === 'vitest.dev' && path.startsWith('/config/')));
|
|
16
|
+
}
|
|
17
|
+
function isOfficialDocs(host, path) {
|
|
18
|
+
return ((host === 'playwright.dev' && path.startsWith('/docs/')) ||
|
|
19
|
+
(host === 'vitest.dev' && path.startsWith('/guide/')) ||
|
|
20
|
+
(host === 'github.com' && path.startsWith('/vitest-dev/vitest/') && path.includes('/docs/')) ||
|
|
21
|
+
host === 'learn.microsoft.com');
|
|
22
|
+
}
|
|
23
|
+
function isIssueThread(host, path) {
|
|
24
|
+
return host === 'github.com' && (path.includes('/issues/') || path.includes('/discussions/'));
|
|
25
|
+
}
|
|
26
|
+
function hasForumThreadPath(path) {
|
|
27
|
+
return (path.includes('/forum/') ||
|
|
28
|
+
path.includes('/forums/') ||
|
|
29
|
+
path.includes('/t/') ||
|
|
30
|
+
path.includes('/topic/') ||
|
|
31
|
+
path.includes('/threads/'));
|
|
32
|
+
}
|
|
33
|
+
function isForumThread(host, path) {
|
|
34
|
+
return ((host === 'reddit.com' && path.includes('/comments/')) ||
|
|
35
|
+
(host === 'stackoverflow.com' && path.startsWith('/questions/')) ||
|
|
36
|
+
(COMMUNITY_FORUM_HOST_RE.test(`${host}.`) && hasForumThreadPath(path)));
|
|
37
|
+
}
|
|
38
|
+
export function classifySourceProfile(rawUrl) {
|
|
39
|
+
const parsed = parseUrl(rawUrl);
|
|
40
|
+
if (!parsed)
|
|
41
|
+
return profile('community', 'community', false);
|
|
42
|
+
const host = parsed.hostname.toLowerCase().replace(/^www\./, '');
|
|
43
|
+
const path = parsed.pathname.toLowerCase();
|
|
44
|
+
if (isOfficialApi(host, path)) {
|
|
45
|
+
return profile('official-api', 'official-api', false);
|
|
46
|
+
}
|
|
47
|
+
if (isOfficialDocs(host, path)) {
|
|
48
|
+
return profile('official-docs', 'official-docs', false);
|
|
49
|
+
}
|
|
50
|
+
if (isIssueThread(host, path)) {
|
|
51
|
+
return profile('issue-thread', 'issue-thread', true);
|
|
52
|
+
}
|
|
53
|
+
if (isForumThread(host, path)) {
|
|
54
|
+
return profile('forum-thread', 'community', true);
|
|
55
|
+
}
|
|
56
|
+
if (host === 'npmjs.com' && path.startsWith('/package/')) {
|
|
57
|
+
return profile('package-page', 'package-page', false);
|
|
58
|
+
}
|
|
59
|
+
return profile('community', 'community', false);
|
|
60
|
+
}
|
|
@@ -23,4 +23,5 @@ export declare function getPresentationConfigPaths(options?: PresentationConfigS
|
|
|
23
23
|
};
|
|
24
24
|
export declare function loadPresentationConfigLayers(options?: PresentationConfigStoreOptions): Promise<LoadedPresentationConfig>;
|
|
25
25
|
export declare function savePresentationConfigScope(options: PresentationConfigStoreOptions, scope: PresentationScope, config: PresentationConfigOverride): Promise<void>;
|
|
26
|
+
export declare function saveBackendConfigScope(options: PresentationConfigStoreOptions, scope: PresentationScope, config: BackendConfigOverride): Promise<void>;
|
|
26
27
|
export declare function resetPresentationConfigScope(options: PresentationConfigStoreOptions, scope: PresentationScope): Promise<void>;
|
|
@@ -49,6 +49,35 @@ function serializePresentationConfigOverride(config) {
|
|
|
49
49
|
}
|
|
50
50
|
return { presentation };
|
|
51
51
|
}
|
|
52
|
+
function serializeBackendConfigOverride(config) {
|
|
53
|
+
const backends = {};
|
|
54
|
+
if (config.search && Object.keys(config.search).length > 0) {
|
|
55
|
+
backends.search = { ...config.search };
|
|
56
|
+
}
|
|
57
|
+
if (config.fetch && Object.keys(config.fetch).length > 0) {
|
|
58
|
+
const { apiKey: _apiKey, ...fetch } = config.fetch;
|
|
59
|
+
backends.fetch = { ...fetch };
|
|
60
|
+
}
|
|
61
|
+
if (config.headless && Object.keys(config.headless).length > 0) {
|
|
62
|
+
backends.headless = { ...config.headless };
|
|
63
|
+
}
|
|
64
|
+
return { backends };
|
|
65
|
+
}
|
|
66
|
+
async function readConfigFileForWrite(filePath) {
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error?.code === 'ENOENT') {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function writeConfigFile(filePath, config) {
|
|
78
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
79
|
+
await writeFile(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
80
|
+
}
|
|
52
81
|
export async function loadPresentationConfigLayers(options = {}) {
|
|
53
82
|
const { globalPath, projectPath } = getPresentationConfigPaths(options);
|
|
54
83
|
const global = await readPresentationConfigFile(globalPath);
|
|
@@ -63,8 +92,20 @@ export async function loadPresentationConfigLayers(options = {}) {
|
|
|
63
92
|
export async function savePresentationConfigScope(options, scope, config) {
|
|
64
93
|
const { globalPath, projectPath } = getPresentationConfigPaths(options);
|
|
65
94
|
const filePath = scope === 'global' ? globalPath : projectPath;
|
|
66
|
-
await
|
|
67
|
-
await
|
|
95
|
+
const existing = await readConfigFileForWrite(filePath);
|
|
96
|
+
await writeConfigFile(filePath, {
|
|
97
|
+
...existing,
|
|
98
|
+
...serializePresentationConfigOverride(config)
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
export async function saveBackendConfigScope(options, scope, config) {
|
|
102
|
+
const { globalPath, projectPath } = getPresentationConfigPaths(options);
|
|
103
|
+
const filePath = scope === 'global' ? globalPath : projectPath;
|
|
104
|
+
const existing = await readConfigFileForWrite(filePath);
|
|
105
|
+
await writeConfigFile(filePath, {
|
|
106
|
+
...existing,
|
|
107
|
+
...serializeBackendConfigOverride(config)
|
|
108
|
+
});
|
|
68
109
|
}
|
|
69
110
|
export async function resetPresentationConfigScope(options, scope) {
|
|
70
111
|
const { globalPath, projectPath } = getPresentationConfigPaths(options);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@demigodmode/pi-web-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"@mozilla/readability": "^0.6.0",
|
|
58
58
|
"cheerio": "^1.1.0",
|
|
59
59
|
"jsdom": "^26.0.0",
|
|
60
|
-
"playwright
|
|
60
|
+
"playwright": "^1.60.0",
|
|
61
61
|
"typebox": "^1.1.37"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|