@demigodmode/pi-web-agent 1.0.0 → 1.2.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 +29 -0
- package/README.md +11 -1
- package/dist/backends/config.d.ts +17 -0
- package/dist/backends/config.js +65 -0
- package/dist/backends/doctor.js +22 -3
- package/dist/backends/factory.d.ts +13 -1
- package/dist/backends/factory.js +58 -11
- package/dist/commands/web-agent-config.d.ts +14 -1
- package/dist/commands/web-agent-config.js +336 -19
- package/dist/fetch/firecrawl-fetch.d.ts +3 -1
- package/dist/fetch/firecrawl-fetch.js +7 -2
- package/dist/presentation/config-store.d.ts +1 -0
- package/dist/presentation/config-store.js +43 -2
- package/dist/presentation/fetch-presentation.js +6 -3
- package/dist/presentation/search-presentation.js +5 -2
- package/dist/search/searxng.d.ts +3 -1
- package/dist/search/searxng.js +9 -3
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,35 @@ The format is intentionally simple and release-oriented.
|
|
|
18
18
|
### Breaking
|
|
19
19
|
- None.
|
|
20
20
|
|
|
21
|
+
## [1.2.0] - 2026-06-01
|
|
22
|
+
### Added
|
|
23
|
+
- Added backend provider and fallback editing to `/web-agent settings`.
|
|
24
|
+
- Added interactive URL prompts for SearXNG and Firecrawl backend setup.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- Nothing yet.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- Nothing yet.
|
|
31
|
+
|
|
32
|
+
### Breaking
|
|
33
|
+
- None.
|
|
34
|
+
|
|
35
|
+
## [1.1.0] - 2026-05-25
|
|
36
|
+
### Added
|
|
37
|
+
- Added explicit opt-in fallback from SearXNG to DuckDuckGo and Firecrawl to HTTP.
|
|
38
|
+
- Added supported SearXNG and Firecrawl backend options in config.
|
|
39
|
+
- Added env-gated live tests for local SearXNG and Firecrawl instances.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- `/web-agent show` and `/web-agent doctor` now report configured backend fallback/options.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
- Nothing yet.
|
|
46
|
+
|
|
47
|
+
### Breaking
|
|
48
|
+
- None.
|
|
49
|
+
|
|
21
50
|
## [1.0.0] - 2026-05-08
|
|
22
51
|
### Added
|
|
23
52
|
- Added one-time `pi-web-agent` changelog notices after package updates and `/web-agent changelog` for manual viewing.
|
package/README.md
CHANGED
|
@@ -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 browser headless fallback.
|
|
112
|
+
Backend config is also supported. Defaults remain DuckDuckGo search, plain HTTP fetch, and local browser headless fallback.
|
|
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
|
|
|
@@ -1,11 +1,24 @@
|
|
|
1
|
+
export type SearxngOptions = {
|
|
2
|
+
categories?: string[];
|
|
3
|
+
language?: string;
|
|
4
|
+
safesearch?: 0 | 1 | 2;
|
|
5
|
+
};
|
|
6
|
+
export type FirecrawlOptions = {
|
|
7
|
+
formats?: string[];
|
|
8
|
+
onlyMainContent?: boolean;
|
|
9
|
+
};
|
|
1
10
|
export type SearchBackendConfig = {
|
|
2
11
|
provider: 'duckduckgo' | 'searxng';
|
|
3
12
|
baseUrl?: string;
|
|
13
|
+
fallback?: 'duckduckgo';
|
|
14
|
+
options?: SearxngOptions;
|
|
4
15
|
};
|
|
5
16
|
export type FetchBackendConfig = {
|
|
6
17
|
provider: 'http' | 'firecrawl';
|
|
7
18
|
baseUrl?: string;
|
|
8
19
|
apiKey?: string;
|
|
20
|
+
fallback?: 'http';
|
|
21
|
+
options?: FirecrawlOptions;
|
|
9
22
|
};
|
|
10
23
|
export type HeadlessBackendConfig = {
|
|
11
24
|
provider: 'local-browser';
|
|
@@ -25,11 +38,15 @@ export type BackendConfigFile = {
|
|
|
25
38
|
search?: {
|
|
26
39
|
provider?: unknown;
|
|
27
40
|
baseUrl?: unknown;
|
|
41
|
+
fallback?: unknown;
|
|
42
|
+
options?: unknown;
|
|
28
43
|
};
|
|
29
44
|
fetch?: {
|
|
30
45
|
provider?: unknown;
|
|
31
46
|
baseUrl?: unknown;
|
|
32
47
|
apiKey?: unknown;
|
|
48
|
+
fallback?: unknown;
|
|
49
|
+
options?: unknown;
|
|
33
50
|
};
|
|
34
51
|
headless?: {
|
|
35
52
|
provider?: unknown;
|
package/dist/backends/config.js
CHANGED
|
@@ -3,6 +3,38 @@ export const DEFAULT_BACKEND_CONFIG = {
|
|
|
3
3
|
fetch: { provider: 'http' },
|
|
4
4
|
headless: { provider: 'local-browser' }
|
|
5
5
|
};
|
|
6
|
+
function extractStringArray(value) {
|
|
7
|
+
if (!Array.isArray(value))
|
|
8
|
+
return undefined;
|
|
9
|
+
const strings = value.filter((item) => typeof item === 'string');
|
|
10
|
+
return strings.length === value.length ? strings : undefined;
|
|
11
|
+
}
|
|
12
|
+
function extractSearxngOptions(value) {
|
|
13
|
+
if (!value || typeof value !== 'object')
|
|
14
|
+
return undefined;
|
|
15
|
+
const raw = value;
|
|
16
|
+
const options = {};
|
|
17
|
+
const categories = extractStringArray(raw.categories);
|
|
18
|
+
if (categories)
|
|
19
|
+
options.categories = categories;
|
|
20
|
+
if (typeof raw.language === 'string')
|
|
21
|
+
options.language = raw.language;
|
|
22
|
+
if (raw.safesearch === 0 || raw.safesearch === 1 || raw.safesearch === 2)
|
|
23
|
+
options.safesearch = raw.safesearch;
|
|
24
|
+
return Object.keys(options).length > 0 ? options : undefined;
|
|
25
|
+
}
|
|
26
|
+
function extractFirecrawlOptions(value) {
|
|
27
|
+
if (!value || typeof value !== 'object')
|
|
28
|
+
return undefined;
|
|
29
|
+
const raw = value;
|
|
30
|
+
const options = {};
|
|
31
|
+
const formats = extractStringArray(raw.formats);
|
|
32
|
+
if (formats)
|
|
33
|
+
options.formats = formats;
|
|
34
|
+
if (typeof raw.onlyMainContent === 'boolean')
|
|
35
|
+
options.onlyMainContent = raw.onlyMainContent;
|
|
36
|
+
return Object.keys(options).length > 0 ? options : undefined;
|
|
37
|
+
}
|
|
6
38
|
export function extractBackendConfigOverride(file) {
|
|
7
39
|
const backends = file?.backends;
|
|
8
40
|
const override = {};
|
|
@@ -11,6 +43,13 @@ export function extractBackendConfigOverride(file) {
|
|
|
11
43
|
if (typeof backends.search.baseUrl === 'string') {
|
|
12
44
|
override.search.baseUrl = backends.search.baseUrl;
|
|
13
45
|
}
|
|
46
|
+
if (backends.search.fallback === 'duckduckgo') {
|
|
47
|
+
override.search.fallback = 'duckduckgo';
|
|
48
|
+
}
|
|
49
|
+
const options = extractSearxngOptions(backends.search.options);
|
|
50
|
+
if (options) {
|
|
51
|
+
override.search.options = options;
|
|
52
|
+
}
|
|
14
53
|
}
|
|
15
54
|
if (backends?.fetch?.provider === 'http' || backends?.fetch?.provider === 'firecrawl') {
|
|
16
55
|
override.fetch = { provider: backends.fetch.provider };
|
|
@@ -20,6 +59,13 @@ export function extractBackendConfigOverride(file) {
|
|
|
20
59
|
if (typeof backends.fetch.apiKey === 'string') {
|
|
21
60
|
override.fetch.apiKey = backends.fetch.apiKey;
|
|
22
61
|
}
|
|
62
|
+
if (backends.fetch.fallback === 'http') {
|
|
63
|
+
override.fetch.fallback = 'http';
|
|
64
|
+
}
|
|
65
|
+
const options = extractFirecrawlOptions(backends.fetch.options);
|
|
66
|
+
if (options) {
|
|
67
|
+
override.fetch.options = options;
|
|
68
|
+
}
|
|
23
69
|
}
|
|
24
70
|
if (backends?.headless?.provider === 'local-browser') {
|
|
25
71
|
override.headless = { provider: 'local-browser' };
|
|
@@ -34,6 +80,25 @@ export function validateBackendConfig(config) {
|
|
|
34
80
|
if (config.fetch.provider === 'firecrawl' && !config.fetch.baseUrl) {
|
|
35
81
|
issues.push('fetch provider firecrawl requires backends.fetch.baseUrl');
|
|
36
82
|
}
|
|
83
|
+
if (config.search.fallback === 'duckduckgo' && config.search.provider !== 'searxng') {
|
|
84
|
+
issues.push('search fallback duckduckgo is only supported when search provider is searxng');
|
|
85
|
+
}
|
|
86
|
+
if (config.fetch.fallback === 'http' && config.fetch.provider !== 'firecrawl') {
|
|
87
|
+
issues.push('fetch fallback http is only supported when fetch provider is firecrawl');
|
|
88
|
+
}
|
|
89
|
+
if (config.search.options?.categories && config.search.options.categories.length === 0) {
|
|
90
|
+
issues.push('search options.categories must contain at least one category when provided');
|
|
91
|
+
}
|
|
92
|
+
if (config.search.options?.language !== undefined && !config.search.options.language.trim()) {
|
|
93
|
+
issues.push('search options.language must not be empty when provided');
|
|
94
|
+
}
|
|
95
|
+
if (config.search.options?.safesearch !== undefined &&
|
|
96
|
+
![0, 1, 2].includes(config.search.options.safesearch)) {
|
|
97
|
+
issues.push('search options.safesearch must be 0, 1, or 2 when provided');
|
|
98
|
+
}
|
|
99
|
+
if (config.fetch.options?.formats && config.fetch.options.formats.length === 0) {
|
|
100
|
+
issues.push('fetch options.formats must contain at least one format when provided');
|
|
101
|
+
}
|
|
37
102
|
return issues;
|
|
38
103
|
}
|
|
39
104
|
function mergeSearchConfig(current, override) {
|
package/dist/backends/doctor.js
CHANGED
|
@@ -6,12 +6,25 @@ function withTimeout(timeoutMs) {
|
|
|
6
6
|
function message(error) {
|
|
7
7
|
return error instanceof Error ? error.message : String(error);
|
|
8
8
|
}
|
|
9
|
-
function searxngDoctorUrl(baseUrl) {
|
|
9
|
+
function searxngDoctorUrl(baseUrl, options = {}) {
|
|
10
10
|
const url = new URL('/search', baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);
|
|
11
11
|
url.searchParams.set('q', 'pi-web-agent-doctor');
|
|
12
12
|
url.searchParams.set('format', 'json');
|
|
13
|
+
if (options.categories?.length)
|
|
14
|
+
url.searchParams.set('categories', options.categories.join(','));
|
|
15
|
+
if (options.language)
|
|
16
|
+
url.searchParams.set('language', options.language);
|
|
17
|
+
if (options.safesearch !== undefined)
|
|
18
|
+
url.searchParams.set('safesearch', String(options.safesearch));
|
|
13
19
|
return url.toString();
|
|
14
20
|
}
|
|
21
|
+
function firecrawlDoctorBody(options = {}) {
|
|
22
|
+
return {
|
|
23
|
+
url: 'https://example.com',
|
|
24
|
+
formats: options.formats ?? ['markdown'],
|
|
25
|
+
...(options.onlyMainContent !== undefined ? { onlyMainContent: options.onlyMainContent } : {})
|
|
26
|
+
};
|
|
27
|
+
}
|
|
15
28
|
export async function checkBackendHealth(config, { fetchImpl = fetch, timeoutMs = 3_000 } = {}) {
|
|
16
29
|
const lines = [];
|
|
17
30
|
if (config.search.provider === 'duckduckgo') {
|
|
@@ -23,7 +36,7 @@ export async function checkBackendHealth(config, { fetchImpl = fetch, timeoutMs
|
|
|
23
36
|
else {
|
|
24
37
|
const timeout = withTimeout(timeoutMs);
|
|
25
38
|
try {
|
|
26
|
-
const response = await fetchImpl(searxngDoctorUrl(config.search.baseUrl), { signal: timeout.signal });
|
|
39
|
+
const response = await fetchImpl(searxngDoctorUrl(config.search.baseUrl, config.search.options), { signal: timeout.signal });
|
|
27
40
|
const json = (await response.json());
|
|
28
41
|
lines.push(response.ok && Array.isArray(json.results)
|
|
29
42
|
? 'search backend: searxng ok'
|
|
@@ -36,6 +49,9 @@ export async function checkBackendHealth(config, { fetchImpl = fetch, timeoutMs
|
|
|
36
49
|
timeout.done();
|
|
37
50
|
}
|
|
38
51
|
}
|
|
52
|
+
if (config.search.fallback) {
|
|
53
|
+
lines.push(`search fallback: ${config.search.fallback}`);
|
|
54
|
+
}
|
|
39
55
|
if (config.fetch.provider === 'http') {
|
|
40
56
|
lines.push('fetch backend: http');
|
|
41
57
|
}
|
|
@@ -52,7 +68,7 @@ export async function checkBackendHealth(config, { fetchImpl = fetch, timeoutMs
|
|
|
52
68
|
const response = await fetchImpl(new URL('/v1/scrape', config.fetch.baseUrl).toString(), {
|
|
53
69
|
method: 'POST',
|
|
54
70
|
headers,
|
|
55
|
-
body: JSON.stringify(
|
|
71
|
+
body: JSON.stringify(firecrawlDoctorBody(config.fetch.options)),
|
|
56
72
|
signal: timeout.signal
|
|
57
73
|
});
|
|
58
74
|
lines.push(response.ok ? 'fetch backend: firecrawl ok' : `fetch backend: firecrawl warning (HTTP ${response.status})`);
|
|
@@ -64,5 +80,8 @@ export async function checkBackendHealth(config, { fetchImpl = fetch, timeoutMs
|
|
|
64
80
|
timeout.done();
|
|
65
81
|
}
|
|
66
82
|
}
|
|
83
|
+
if (config.fetch.fallback) {
|
|
84
|
+
lines.push(`fetch fallback: ${config.fetch.fallback}`);
|
|
85
|
+
}
|
|
67
86
|
return lines;
|
|
68
87
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { createFirecrawlFetcher } from '../fetch/firecrawl-fetch.js';
|
|
2
|
+
import { createSearxngSearchTool } from '../search/searxng.js';
|
|
3
|
+
import { createWebFetchHeadlessTool } from '../tools/web-fetch-headless.js';
|
|
4
|
+
import { createWebFetchTool } from '../tools/web-fetch.js';
|
|
5
|
+
import { createWebSearchTool } from '../tools/web-search.js';
|
|
1
6
|
import type { WebFetchHeadlessResponse, WebFetchResponse, WebSearchResponse } from '../types.js';
|
|
2
7
|
import { type BackendConfig } from './config.js';
|
|
3
8
|
export type BackendSet = {
|
|
@@ -11,4 +16,11 @@ export type BackendSet = {
|
|
|
11
16
|
url: string;
|
|
12
17
|
}) => Promise<WebFetchHeadlessResponse>;
|
|
13
18
|
};
|
|
14
|
-
export
|
|
19
|
+
export type BackendFactoryDeps = {
|
|
20
|
+
createDuckDuckGoSearch?: typeof createWebSearchTool;
|
|
21
|
+
createSearxngSearch?: typeof createSearxngSearchTool;
|
|
22
|
+
createHttpFetch?: typeof createWebFetchTool;
|
|
23
|
+
createFirecrawlFetch?: typeof createFirecrawlFetcher;
|
|
24
|
+
createHeadlessFetch?: typeof createWebFetchHeadlessTool;
|
|
25
|
+
};
|
|
26
|
+
export declare function createBackendSet(config?: BackendConfig, deps?: BackendFactoryDeps): BackendSet;
|
package/dist/backends/factory.js
CHANGED
|
@@ -34,25 +34,72 @@ function invalidFirecrawlFetch() {
|
|
|
34
34
|
return { ...result, presentation: buildFetchPresentation(result) };
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
function withSearchFallback(primary, fallback) {
|
|
38
|
+
return async (input) => {
|
|
39
|
+
const first = await primary(input);
|
|
40
|
+
if (first.status !== 'error')
|
|
41
|
+
return first;
|
|
42
|
+
const second = await fallback(input);
|
|
43
|
+
const result = {
|
|
44
|
+
...second,
|
|
45
|
+
metadata: {
|
|
46
|
+
...second.metadata,
|
|
47
|
+
fallbackFrom: 'searxng',
|
|
48
|
+
fallbackReason: first.error?.message ?? 'SearXNG search failed.'
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
return { ...result, presentation: buildSearchPresentation(result) };
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function withFetchFallback(primary, fallback) {
|
|
55
|
+
return async (input) => {
|
|
56
|
+
const first = await primary(input);
|
|
57
|
+
if (first.status !== 'error' && first.status !== 'needs_headless')
|
|
58
|
+
return first;
|
|
59
|
+
const second = await fallback(input);
|
|
60
|
+
const result = {
|
|
61
|
+
...second,
|
|
62
|
+
metadata: {
|
|
63
|
+
...second.metadata,
|
|
64
|
+
fallbackFrom: 'firecrawl',
|
|
65
|
+
fallbackReason: first.error?.message ?? 'Firecrawl fetch failed.'
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
return { ...result, presentation: buildFetchPresentation(result) };
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function createBackendSet(config = DEFAULT_BACKEND_CONFIG, deps = {}) {
|
|
72
|
+
const createDuckDuckGoSearch = deps.createDuckDuckGoSearch ?? createWebSearchTool;
|
|
73
|
+
const createSearxngSearch = deps.createSearxngSearch ?? createSearxngSearchTool;
|
|
74
|
+
const createHttpFetch = deps.createHttpFetch ?? createWebFetchTool;
|
|
75
|
+
const createFirecrawlFetch = deps.createFirecrawlFetch ?? createFirecrawlFetcher;
|
|
76
|
+
const createHeadlessFetch = deps.createHeadlessFetch ?? createWebFetchHeadlessTool;
|
|
77
|
+
let search = config.search.provider === 'searxng'
|
|
39
78
|
? config.search.baseUrl
|
|
40
|
-
?
|
|
79
|
+
? createSearxngSearch({ baseUrl: config.search.baseUrl, options: config.search.options })
|
|
41
80
|
: invalidSearxngSearch()
|
|
42
|
-
:
|
|
43
|
-
|
|
81
|
+
: createDuckDuckGoSearch();
|
|
82
|
+
if (config.search.provider === 'searxng' && config.search.fallback === 'duckduckgo') {
|
|
83
|
+
search = withSearchFallback(search, createDuckDuckGoSearch());
|
|
84
|
+
}
|
|
85
|
+
const httpFetch = createHttpFetch();
|
|
86
|
+
let fetchPage = config.fetch.provider === 'firecrawl'
|
|
44
87
|
? config.fetch.baseUrl
|
|
45
|
-
?
|
|
46
|
-
fetchPage:
|
|
88
|
+
? createHttpFetch({
|
|
89
|
+
fetchPage: createFirecrawlFetch({
|
|
47
90
|
baseUrl: config.fetch.baseUrl,
|
|
48
|
-
apiKey: config.fetch.apiKey ?? process.env.PI_WEB_AGENT_FIRECRAWL_API_KEY
|
|
91
|
+
apiKey: config.fetch.apiKey ?? process.env.PI_WEB_AGENT_FIRECRAWL_API_KEY,
|
|
92
|
+
options: config.fetch.options
|
|
49
93
|
})
|
|
50
94
|
})
|
|
51
|
-
:
|
|
52
|
-
:
|
|
95
|
+
: createHttpFetch({ fetchPage: invalidFirecrawlFetch() })
|
|
96
|
+
: httpFetch;
|
|
97
|
+
if (config.fetch.provider === 'firecrawl' && config.fetch.fallback === 'http') {
|
|
98
|
+
fetchPage = withFetchFallback(fetchPage, httpFetch);
|
|
99
|
+
}
|
|
53
100
|
return {
|
|
54
101
|
search,
|
|
55
102
|
fetchPage,
|
|
56
|
-
headlessFetch:
|
|
103
|
+
headlessFetch: createHeadlessFetch()
|
|
57
104
|
};
|
|
58
105
|
}
|
|
@@ -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');
|
|
@@ -25,16 +53,33 @@ async function defaultCheckTypebox() {
|
|
|
25
53
|
return false;
|
|
26
54
|
}
|
|
27
55
|
}
|
|
56
|
+
function formatSearchOptions(config) {
|
|
57
|
+
return [
|
|
58
|
+
config.fallback ? `fallback ${config.fallback}` : undefined,
|
|
59
|
+
config.options?.categories?.length ? `categories ${config.options.categories.join(',')}` : undefined,
|
|
60
|
+
config.options?.language ? `language ${config.options.language}` : undefined,
|
|
61
|
+
config.options?.safesearch !== undefined ? `safesearch ${config.options.safesearch}` : undefined
|
|
62
|
+
].filter(Boolean).join(' ');
|
|
63
|
+
}
|
|
64
|
+
function formatFetchOptions(config) {
|
|
65
|
+
return [
|
|
66
|
+
config.fallback ? `fallback ${config.fallback}` : undefined,
|
|
67
|
+
config.options?.formats?.length ? `formats ${config.options.formats.join(',')}` : undefined,
|
|
68
|
+
config.options?.onlyMainContent !== undefined ? `onlyMainContent ${config.options.onlyMainContent}` : undefined
|
|
69
|
+
].filter(Boolean).join(' ');
|
|
70
|
+
}
|
|
28
71
|
function formatBackendSummary(config = DEFAULT_BACKEND_CONFIG) {
|
|
29
|
-
const
|
|
72
|
+
const searchSuffix = formatSearchOptions(config.search);
|
|
73
|
+
const fetchSuffix = formatFetchOptions(config.fetch);
|
|
74
|
+
const searchBase = config.search.baseUrl
|
|
30
75
|
? `search: ${config.search.provider} (${config.search.baseUrl})`
|
|
31
76
|
: `search: ${config.search.provider}`;
|
|
32
|
-
const
|
|
77
|
+
const fetchBase = config.fetch.baseUrl
|
|
33
78
|
? `fetch: ${config.fetch.provider} (${config.fetch.baseUrl})`
|
|
34
79
|
: `fetch: ${config.fetch.provider}`;
|
|
35
80
|
return [
|
|
36
|
-
|
|
37
|
-
|
|
81
|
+
searchSuffix ? `${searchBase} ${searchSuffix}` : searchBase,
|
|
82
|
+
fetchSuffix ? `${fetchBase} ${fetchSuffix}` : fetchBase,
|
|
38
83
|
`headless: ${config.headless.provider}`
|
|
39
84
|
].join('\n');
|
|
40
85
|
}
|
|
@@ -45,7 +90,7 @@ function formatConfigSummary(config) {
|
|
|
45
90
|
}
|
|
46
91
|
return lines.join('\n');
|
|
47
92
|
}
|
|
48
|
-
function
|
|
93
|
+
function buildPresentationSettingsItems(scope, config) {
|
|
49
94
|
return [
|
|
50
95
|
{
|
|
51
96
|
id: 'scope',
|
|
@@ -67,6 +112,58 @@ function buildSettingsItems(scope, config) {
|
|
|
67
112
|
}))
|
|
68
113
|
];
|
|
69
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
|
+
}
|
|
70
167
|
function isToolName(value) {
|
|
71
168
|
return PRESENTATION_TOOL_NAMES.includes(value);
|
|
72
169
|
}
|
|
@@ -85,15 +182,33 @@ export function getScopeDisplayConfig(loaded, scope) {
|
|
|
85
182
|
}
|
|
86
183
|
return mergePresentationConfigLayers(DEFAULT_PRESENTATION_CONFIG, loaded.global.rawConfig, loaded.project.rawConfig);
|
|
87
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
|
+
}
|
|
88
197
|
export function createSettingsDraftState(loaded, initialScope) {
|
|
89
198
|
const drafts = {
|
|
90
199
|
global: getScopeDisplayConfig(loaded, 'global'),
|
|
91
200
|
project: getScopeDisplayConfig(loaded, 'project')
|
|
92
201
|
};
|
|
202
|
+
const backendDrafts = {
|
|
203
|
+
global: getScopeDisplayBackends(loaded, 'global'),
|
|
204
|
+
project: getScopeDisplayBackends(loaded, 'project')
|
|
205
|
+
};
|
|
93
206
|
return {
|
|
94
207
|
scope: initialScope,
|
|
95
208
|
drafts,
|
|
96
|
-
|
|
209
|
+
backendDrafts,
|
|
210
|
+
config: clonePresentationConfig(drafts[initialScope]),
|
|
211
|
+
backends: cloneBackendConfig(backendDrafts[initialScope])
|
|
97
212
|
};
|
|
98
213
|
}
|
|
99
214
|
export function applySettingsValue(state, id, newValue) {
|
|
@@ -101,16 +216,23 @@ export function applySettingsValue(state, id, newValue) {
|
|
|
101
216
|
global: clonePresentationConfig(state.drafts.global),
|
|
102
217
|
project: clonePresentationConfig(state.drafts.project)
|
|
103
218
|
};
|
|
219
|
+
const nextBackendDrafts = {
|
|
220
|
+
global: cloneBackendConfig(state.backendDrafts.global),
|
|
221
|
+
project: cloneBackendConfig(state.backendDrafts.project)
|
|
222
|
+
};
|
|
104
223
|
let nextScope = state.scope;
|
|
105
224
|
if (id === 'scope' && (newValue === 'project' || newValue === 'global')) {
|
|
106
225
|
nextScope = newValue;
|
|
107
226
|
return {
|
|
108
227
|
scope: nextScope,
|
|
109
228
|
drafts: nextDrafts,
|
|
110
|
-
|
|
229
|
+
backendDrafts: nextBackendDrafts,
|
|
230
|
+
config: clonePresentationConfig(nextDrafts[nextScope]),
|
|
231
|
+
backends: cloneBackendConfig(nextBackendDrafts[nextScope])
|
|
111
232
|
};
|
|
112
233
|
}
|
|
113
234
|
const currentDraft = clonePresentationConfig(nextDrafts[nextScope]);
|
|
235
|
+
const currentBackends = cloneBackendConfig(nextBackendDrafts[nextScope]);
|
|
114
236
|
if (id === 'defaultMode' && (newValue === 'compact' || newValue === 'preview' || newValue === 'verbose')) {
|
|
115
237
|
currentDraft.defaultMode = newValue;
|
|
116
238
|
}
|
|
@@ -127,11 +249,58 @@ export function applySettingsValue(state, id, newValue) {
|
|
|
127
249
|
}
|
|
128
250
|
currentDraft.tools = nextTools;
|
|
129
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
|
+
}
|
|
130
296
|
nextDrafts[nextScope] = currentDraft;
|
|
297
|
+
nextBackendDrafts[nextScope] = currentBackends;
|
|
131
298
|
return {
|
|
132
299
|
scope: nextScope,
|
|
133
300
|
drafts: nextDrafts,
|
|
134
|
-
|
|
301
|
+
backendDrafts: nextBackendDrafts,
|
|
302
|
+
config: clonePresentationConfig(nextDrafts[nextScope]),
|
|
303
|
+
backends: cloneBackendConfig(nextBackendDrafts[nextScope])
|
|
135
304
|
};
|
|
136
305
|
}
|
|
137
306
|
export function collapsePresentationConfigToOverride(config, inheritedConfig) {
|
|
@@ -151,6 +320,44 @@ export function collapsePresentationConfigToOverride(config, inheritedConfig) {
|
|
|
151
320
|
tools
|
|
152
321
|
};
|
|
153
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
|
+
}
|
|
154
361
|
export function handleSettingsShortcut(data) {
|
|
155
362
|
if (data === '\u001b') {
|
|
156
363
|
return { action: 'cancel' };
|
|
@@ -167,9 +374,10 @@ async function openActionMenu(ctx) {
|
|
|
167
374
|
return ctx.ui.custom((tui, theme, _kb, done) => {
|
|
168
375
|
const container = new Container();
|
|
169
376
|
const items = [
|
|
170
|
-
{ value: 'settings', label: 'Settings', description: 'Edit presentation modes' },
|
|
377
|
+
{ value: 'settings', label: 'Settings', description: 'Edit presentation modes and backends' },
|
|
171
378
|
{ value: 'show', label: 'Show config', description: 'Print effective config paths and modes' },
|
|
172
379
|
{ value: 'doctor', label: 'Doctor', description: 'Check runtime dependencies and browser detection' },
|
|
380
|
+
{ value: 'changelog', label: 'Changelog', description: 'Show latest package changelog' },
|
|
173
381
|
{ value: 'reset-project', label: 'Reset project config', description: 'Delete project-level overrides' },
|
|
174
382
|
{ value: 'reset-global', label: 'Reset global config', description: 'Delete global overrides' }
|
|
175
383
|
];
|
|
@@ -197,22 +405,120 @@ async function openActionMenu(ctx) {
|
|
|
197
405
|
};
|
|
198
406
|
});
|
|
199
407
|
}
|
|
200
|
-
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) {
|
|
201
440
|
return ctx.ui.custom((_tui, theme, _kb, done) => {
|
|
202
441
|
let state = createSettingsDraftState(loaded, initialScope);
|
|
203
442
|
let settingsList;
|
|
204
443
|
const container = new Container();
|
|
205
|
-
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));
|
|
206
445
|
container.addChild(new Text(theme.fg('muted', 'Ctrl+S save · Ctrl+R reset scope · Esc cancel'), 1, 2));
|
|
207
446
|
const rebuildSettingsList = () => {
|
|
208
447
|
if (settingsList) {
|
|
209
448
|
container.removeChild(settingsList);
|
|
210
449
|
}
|
|
211
|
-
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
|
+
}
|
|
212
518
|
state = applySettingsValue(state, id, newValue);
|
|
213
519
|
rebuildSettingsList();
|
|
214
520
|
container.invalidate();
|
|
215
|
-
}, () => 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 });
|
|
216
522
|
container.addChild(settingsList);
|
|
217
523
|
};
|
|
218
524
|
rebuildSettingsList();
|
|
@@ -230,7 +536,7 @@ async function openSettingsUi(ctx, loaded, initialScope) {
|
|
|
230
536
|
return;
|
|
231
537
|
}
|
|
232
538
|
if (shortcut?.action === 'save') {
|
|
233
|
-
done({ action: 'save', scope: state.scope, config: state.config });
|
|
539
|
+
done({ action: 'save', scope: state.scope, config: state.config, backends: state.backends });
|
|
234
540
|
return;
|
|
235
541
|
}
|
|
236
542
|
settingsList.handleInput?.(data);
|
|
@@ -241,6 +547,7 @@ async function openSettingsUi(ctx, loaded, initialScope) {
|
|
|
241
547
|
export function registerWebAgentConfigCommands(pi, deps = {}) {
|
|
242
548
|
const load = deps.load ?? (() => loadPresentationConfigLayers());
|
|
243
549
|
const save = deps.save ?? ((scope, config) => savePresentationConfigScope({}, scope, config));
|
|
550
|
+
const saveBackends = deps.saveBackends ?? ((scope, config) => saveBackendConfigScope({}, scope, config));
|
|
244
551
|
const reset = deps.reset ?? ((scope) => resetPresentationConfigScope({}, scope));
|
|
245
552
|
const resolveBrowser = deps.resolveBrowser ?? (() => resolveBrowserExecutable({}));
|
|
246
553
|
const runtime = deps.runtime ?? {
|
|
@@ -345,7 +652,12 @@ export function registerWebAgentConfigCommands(pi, deps = {}) {
|
|
|
345
652
|
if (!action || action === 'settings') {
|
|
346
653
|
const loaded = await load();
|
|
347
654
|
const initialScope = 'project';
|
|
348
|
-
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);
|
|
349
661
|
if (!result || result.action === 'cancel') {
|
|
350
662
|
return;
|
|
351
663
|
}
|
|
@@ -354,8 +666,13 @@ export function registerWebAgentConfigCommands(pi, deps = {}) {
|
|
|
354
666
|
ctx.ui.notify(`Reset ${result.scope} config`, 'info');
|
|
355
667
|
return;
|
|
356
668
|
}
|
|
357
|
-
|
|
358
|
-
|
|
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');
|
|
359
676
|
return;
|
|
360
677
|
}
|
|
361
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');
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { FirecrawlOptions } from '../backends/config.js';
|
|
1
2
|
import type { WebFetchResponse } from '../types.js';
|
|
2
|
-
export declare function createFirecrawlFetcher({ baseUrl, apiKey, fetchImpl }: {
|
|
3
|
+
export declare function createFirecrawlFetcher({ baseUrl, apiKey, options, fetchImpl }: {
|
|
3
4
|
baseUrl: string;
|
|
4
5
|
apiKey?: string;
|
|
6
|
+
options?: FirecrawlOptions;
|
|
5
7
|
fetchImpl?: typeof fetch;
|
|
6
8
|
}): (url: string) => Promise<WebFetchResponse>;
|
|
@@ -4,17 +4,22 @@ function buildScrapeUrl(baseUrl) {
|
|
|
4
4
|
function errorMessage(error) {
|
|
5
5
|
return error instanceof Error ? error.message : String(error);
|
|
6
6
|
}
|
|
7
|
-
export function createFirecrawlFetcher({ baseUrl, apiKey, fetchImpl = fetch }) {
|
|
7
|
+
export function createFirecrawlFetcher({ baseUrl, apiKey, options, fetchImpl = fetch }) {
|
|
8
8
|
return async function firecrawlFetch(url) {
|
|
9
9
|
try {
|
|
10
10
|
const headers = { 'content-type': 'application/json' };
|
|
11
11
|
if (apiKey) {
|
|
12
12
|
headers.Authorization = `Bearer ${apiKey}`;
|
|
13
13
|
}
|
|
14
|
+
const body = {
|
|
15
|
+
url,
|
|
16
|
+
formats: options?.formats ?? ['markdown'],
|
|
17
|
+
...(options?.onlyMainContent !== undefined ? { onlyMainContent: options.onlyMainContent } : {})
|
|
18
|
+
};
|
|
14
19
|
const response = await fetchImpl(buildScrapeUrl(baseUrl), {
|
|
15
20
|
method: 'POST',
|
|
16
21
|
headers,
|
|
17
|
-
body: JSON.stringify(
|
|
22
|
+
body: JSON.stringify(body)
|
|
18
23
|
});
|
|
19
24
|
if (!response.ok) {
|
|
20
25
|
throw new Error(`HTTP ${response.status}`);
|
|
@@ -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);
|
|
@@ -9,11 +9,14 @@ function firstExcerpt(text, maxChars = 240) {
|
|
|
9
9
|
}
|
|
10
10
|
export function buildFetchPresentation(result) {
|
|
11
11
|
const wordCount = countWords(result.content?.text);
|
|
12
|
+
const fallbackPrefix = result.metadata.fallbackFrom
|
|
13
|
+
? `${result.metadata.fallbackFrom} failed; used ${result.metadata.method} fallback. `
|
|
14
|
+
: '';
|
|
12
15
|
const compact = result.status === 'ok'
|
|
13
|
-
?
|
|
16
|
+
? `${fallbackPrefix}Fetched page · article extracted${wordCount ? ` · ${wordCount} words` : ''}`
|
|
14
17
|
: result.status === 'needs_headless'
|
|
15
|
-
?
|
|
16
|
-
:
|
|
18
|
+
? `${fallbackPrefix}Needs headless rendering: ${result.error?.message ?? 'Headless rendering recommended.'}`
|
|
19
|
+
: `${fallbackPrefix}Fetch failed: ${result.error?.message ?? 'Unknown fetch failure.'}`;
|
|
17
20
|
return {
|
|
18
21
|
mode: 'compact',
|
|
19
22
|
views: {
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
function formatCompact(result) {
|
|
2
|
+
const fallbackPrefix = result.metadata.fallbackFrom
|
|
3
|
+
? `${result.metadata.fallbackFrom} failed; used ${result.metadata.backend} fallback. `
|
|
4
|
+
: '';
|
|
2
5
|
if (result.status === 'error') {
|
|
3
|
-
return
|
|
6
|
+
return `${fallbackPrefix}Search failed: ${result.error?.message ?? 'Unknown search failure.'}`;
|
|
4
7
|
}
|
|
5
8
|
const suffix = result.results.length === 1 ? 'result' : 'results';
|
|
6
|
-
return
|
|
9
|
+
return `${fallbackPrefix}Found ${result.results.length} ${suffix}`;
|
|
7
10
|
}
|
|
8
11
|
export function buildSearchPresentation(result) {
|
|
9
12
|
const preview = result.results
|
package/dist/search/searxng.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { SearxngOptions } from '../backends/config.js';
|
|
1
2
|
import type { WebSearchResponse } from '../types.js';
|
|
2
|
-
export declare function createSearxngSearchTool({ baseUrl, fetchImpl }: {
|
|
3
|
+
export declare function createSearxngSearchTool({ baseUrl, options, fetchImpl }: {
|
|
3
4
|
baseUrl: string;
|
|
5
|
+
options?: SearxngOptions;
|
|
4
6
|
fetchImpl?: typeof fetch;
|
|
5
7
|
}): ({ query }: {
|
|
6
8
|
query: string;
|
package/dist/search/searxng.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { buildSearchPresentation } from '../presentation/search-presentation.js';
|
|
2
|
-
function buildSearchUrl(baseUrl, query) {
|
|
2
|
+
function buildSearchUrl(baseUrl, query, options = {}) {
|
|
3
3
|
const url = new URL('/search', baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);
|
|
4
4
|
url.searchParams.set('q', query);
|
|
5
5
|
url.searchParams.set('format', 'json');
|
|
6
|
+
if (options.categories?.length)
|
|
7
|
+
url.searchParams.set('categories', options.categories.join(','));
|
|
8
|
+
if (options.language)
|
|
9
|
+
url.searchParams.set('language', options.language);
|
|
10
|
+
if (options.safesearch !== undefined)
|
|
11
|
+
url.searchParams.set('safesearch', String(options.safesearch));
|
|
6
12
|
return url.toString();
|
|
7
13
|
}
|
|
8
14
|
function normalizeResults(response) {
|
|
@@ -19,7 +25,7 @@ function normalizeResults(response) {
|
|
|
19
25
|
];
|
|
20
26
|
});
|
|
21
27
|
}
|
|
22
|
-
export function createSearxngSearchTool({ baseUrl, fetchImpl = fetch }) {
|
|
28
|
+
export function createSearxngSearchTool({ baseUrl, options, fetchImpl = fetch }) {
|
|
23
29
|
return async function searxngSearch({ query }) {
|
|
24
30
|
const normalizedQuery = query.trim();
|
|
25
31
|
if (!normalizedQuery) {
|
|
@@ -32,7 +38,7 @@ export function createSearxngSearchTool({ baseUrl, fetchImpl = fetch }) {
|
|
|
32
38
|
return { ...result, presentation: buildSearchPresentation(result) };
|
|
33
39
|
}
|
|
34
40
|
try {
|
|
35
|
-
const response = await fetchImpl(buildSearchUrl(baseUrl, normalizedQuery));
|
|
41
|
+
const response = await fetchImpl(buildSearchUrl(baseUrl, normalizedQuery, options));
|
|
36
42
|
if (!response.ok) {
|
|
37
43
|
throw new Error(`HTTP ${response.status}`);
|
|
38
44
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -13,10 +13,14 @@ export type ToolError = {
|
|
|
13
13
|
export type SearchMetadata = {
|
|
14
14
|
backend: 'duckduckgo' | 'searxng';
|
|
15
15
|
cacheHit: boolean;
|
|
16
|
+
fallbackFrom?: 'searxng';
|
|
17
|
+
fallbackReason?: string;
|
|
16
18
|
};
|
|
17
19
|
export type FetchMetadata = {
|
|
18
20
|
method: 'http' | 'headless' | 'firecrawl';
|
|
19
21
|
cacheHit: boolean;
|
|
22
|
+
fallbackFrom?: 'firecrawl';
|
|
23
|
+
fallbackReason?: string;
|
|
20
24
|
contentType?: string;
|
|
21
25
|
truncated?: boolean;
|
|
22
26
|
browser?: 'configured' | 'chrome' | 'edge' | 'brave' | 'chromium';
|
package/package.json
CHANGED