@demigodmode/pi-web-agent 0.4.0 → 0.6.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/README.md +33 -0
- package/dist/backends/config.d.ts +42 -0
- package/dist/backends/config.js +61 -0
- package/dist/backends/doctor.d.ts +5 -0
- package/dist/backends/doctor.js +68 -0
- package/dist/backends/factory.d.ts +14 -0
- package/dist/backends/factory.js +58 -0
- package/dist/commands/web-agent-config.d.ts +10 -0
- package/dist/commands/web-agent-config.js +112 -4
- package/dist/extension.js +34 -5
- package/dist/fetch/browser-resolution.d.ts +7 -2
- package/dist/fetch/browser-resolution.js +111 -17
- package/dist/fetch/firecrawl-fetch.d.ts +6 -0
- package/dist/fetch/firecrawl-fetch.js +61 -0
- package/dist/orchestration/index.d.ts +3 -1
- package/dist/orchestration/index.js +8 -6
- package/dist/orchestration/research-types.d.ts +1 -1
- package/dist/presentation/config-store.d.ts +3 -0
- package/dist/presentation/config-store.js +11 -2
- package/dist/presentation/explore-presentation.js +2 -0
- package/dist/search/searxng.d.ts +7 -0
- package/dist/search/searxng.js +66 -0
- package/dist/tools/web-explore.d.ts +1 -1
- package/dist/types.d.ts +4 -4
- package/package.json +5 -6
package/README.md
CHANGED
|
@@ -18,6 +18,10 @@ That sounds obvious, but a lot of agent tooling gets fuzzy right there. This pac
|
|
|
18
18
|
pi install npm:@demigodmode/pi-web-agent
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
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 whether headless rendering can find a browser.
|
|
22
|
+
|
|
23
|
+
Headless rendering currently requires a detectable Chromium-family browser: Chrome, Chromium, Edge, or Brave. Firefox/Safari-only systems can still use search and plain HTTP reads, but browser-rendered fallback pages need a supported Chromium-family browser for now.
|
|
24
|
+
|
|
21
25
|
Later on, update installed packages with:
|
|
22
26
|
|
|
23
27
|
```bash
|
|
@@ -65,6 +69,7 @@ Primary UI:
|
|
|
65
69
|
Helper commands:
|
|
66
70
|
|
|
67
71
|
```text
|
|
72
|
+
/web-agent doctor
|
|
68
73
|
/web-agent show
|
|
69
74
|
/web-agent reset project
|
|
70
75
|
/web-agent reset global
|
|
@@ -101,6 +106,34 @@ Example:
|
|
|
101
106
|
}
|
|
102
107
|
```
|
|
103
108
|
|
|
109
|
+
Backend config is also supported. Defaults are DuckDuckGo search, plain HTTP fetch, and local browser headless fallback:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"backends": {
|
|
114
|
+
"search": { "provider": "duckduckgo" },
|
|
115
|
+
"fetch": { "provider": "http" },
|
|
116
|
+
"headless": { "provider": "local-browser" }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Self-hosted example for existing SearXNG/Firecrawl services:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"backends": {
|
|
126
|
+
"search": { "provider": "searxng", "baseUrl": "http://localhost:8080" },
|
|
127
|
+
"fetch": { "provider": "firecrawl", "baseUrl": "http://localhost:3002" },
|
|
128
|
+
"headless": { "provider": "local-browser" }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Prefer `PI_WEB_AGENT_FIRECRAWL_API_KEY` for Firecrawl auth instead of putting secrets in project config. `/web-agent doctor` validates required backend fields and checks configured self-hosted endpoints.
|
|
134
|
+
|
|
135
|
+
Full guide: https://demigodmode.github.io/pi-web-agent/self-hosted-backends
|
|
136
|
+
|
|
104
137
|
## Local development
|
|
105
138
|
|
|
106
139
|
```bash
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type SearchBackendConfig = {
|
|
2
|
+
provider: 'duckduckgo' | 'searxng';
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
};
|
|
5
|
+
export type FetchBackendConfig = {
|
|
6
|
+
provider: 'http' | 'firecrawl';
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
};
|
|
10
|
+
export type HeadlessBackendConfig = {
|
|
11
|
+
provider: 'local-browser';
|
|
12
|
+
};
|
|
13
|
+
export type BackendConfig = {
|
|
14
|
+
search: SearchBackendConfig;
|
|
15
|
+
fetch: FetchBackendConfig;
|
|
16
|
+
headless: HeadlessBackendConfig;
|
|
17
|
+
};
|
|
18
|
+
export type BackendConfigOverride = {
|
|
19
|
+
search?: Partial<SearchBackendConfig>;
|
|
20
|
+
fetch?: Partial<FetchBackendConfig>;
|
|
21
|
+
headless?: Partial<HeadlessBackendConfig>;
|
|
22
|
+
};
|
|
23
|
+
export type BackendConfigFile = {
|
|
24
|
+
backends?: {
|
|
25
|
+
search?: {
|
|
26
|
+
provider?: unknown;
|
|
27
|
+
baseUrl?: unknown;
|
|
28
|
+
};
|
|
29
|
+
fetch?: {
|
|
30
|
+
provider?: unknown;
|
|
31
|
+
baseUrl?: unknown;
|
|
32
|
+
apiKey?: unknown;
|
|
33
|
+
};
|
|
34
|
+
headless?: {
|
|
35
|
+
provider?: unknown;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
export declare const DEFAULT_BACKEND_CONFIG: BackendConfig;
|
|
40
|
+
export declare function extractBackendConfigOverride(file: BackendConfigFile | null | undefined): BackendConfigOverride;
|
|
41
|
+
export declare function validateBackendConfig(config: BackendConfig): string[];
|
|
42
|
+
export declare function mergeBackendConfigLayers(...layers: Array<BackendConfig | BackendConfigOverride | undefined>): BackendConfig;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export const DEFAULT_BACKEND_CONFIG = {
|
|
2
|
+
search: { provider: 'duckduckgo' },
|
|
3
|
+
fetch: { provider: 'http' },
|
|
4
|
+
headless: { provider: 'local-browser' }
|
|
5
|
+
};
|
|
6
|
+
export function extractBackendConfigOverride(file) {
|
|
7
|
+
const backends = file?.backends;
|
|
8
|
+
const override = {};
|
|
9
|
+
if (backends?.search?.provider === 'duckduckgo' || backends?.search?.provider === 'searxng') {
|
|
10
|
+
override.search = { provider: backends.search.provider };
|
|
11
|
+
if (typeof backends.search.baseUrl === 'string') {
|
|
12
|
+
override.search.baseUrl = backends.search.baseUrl;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (backends?.fetch?.provider === 'http' || backends?.fetch?.provider === 'firecrawl') {
|
|
16
|
+
override.fetch = { provider: backends.fetch.provider };
|
|
17
|
+
if (typeof backends.fetch.baseUrl === 'string') {
|
|
18
|
+
override.fetch.baseUrl = backends.fetch.baseUrl;
|
|
19
|
+
}
|
|
20
|
+
if (typeof backends.fetch.apiKey === 'string') {
|
|
21
|
+
override.fetch.apiKey = backends.fetch.apiKey;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (backends?.headless?.provider === 'local-browser') {
|
|
25
|
+
override.headless = { provider: 'local-browser' };
|
|
26
|
+
}
|
|
27
|
+
return override;
|
|
28
|
+
}
|
|
29
|
+
export function validateBackendConfig(config) {
|
|
30
|
+
const issues = [];
|
|
31
|
+
if (config.search.provider === 'searxng' && !config.search.baseUrl) {
|
|
32
|
+
issues.push('search provider searxng requires backends.search.baseUrl');
|
|
33
|
+
}
|
|
34
|
+
if (config.fetch.provider === 'firecrawl' && !config.fetch.baseUrl) {
|
|
35
|
+
issues.push('fetch provider firecrawl requires backends.fetch.baseUrl');
|
|
36
|
+
}
|
|
37
|
+
return issues;
|
|
38
|
+
}
|
|
39
|
+
function mergeSearchConfig(current, override) {
|
|
40
|
+
if (!override)
|
|
41
|
+
return current;
|
|
42
|
+
if (override.provider && override.provider !== current.provider) {
|
|
43
|
+
return { ...override, provider: override.provider };
|
|
44
|
+
}
|
|
45
|
+
return { ...current, ...override };
|
|
46
|
+
}
|
|
47
|
+
function mergeFetchConfig(current, override) {
|
|
48
|
+
if (!override)
|
|
49
|
+
return current;
|
|
50
|
+
if (override.provider && override.provider !== current.provider) {
|
|
51
|
+
return { ...override, provider: override.provider };
|
|
52
|
+
}
|
|
53
|
+
return { ...current, ...override };
|
|
54
|
+
}
|
|
55
|
+
export function mergeBackendConfigLayers(...layers) {
|
|
56
|
+
return layers.reduce((merged, layer) => ({
|
|
57
|
+
search: mergeSearchConfig(merged.search, layer?.search),
|
|
58
|
+
fetch: mergeFetchConfig(merged.fetch, layer?.fetch),
|
|
59
|
+
headless: { ...merged.headless, ...layer?.headless }
|
|
60
|
+
}), DEFAULT_BACKEND_CONFIG);
|
|
61
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
function withTimeout(timeoutMs) {
|
|
2
|
+
const controller = new AbortController();
|
|
3
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
4
|
+
return { signal: controller.signal, done: () => clearTimeout(timeout) };
|
|
5
|
+
}
|
|
6
|
+
function message(error) {
|
|
7
|
+
return error instanceof Error ? error.message : String(error);
|
|
8
|
+
}
|
|
9
|
+
function searxngDoctorUrl(baseUrl) {
|
|
10
|
+
const url = new URL('/search', baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);
|
|
11
|
+
url.searchParams.set('q', 'pi-web-agent-doctor');
|
|
12
|
+
url.searchParams.set('format', 'json');
|
|
13
|
+
return url.toString();
|
|
14
|
+
}
|
|
15
|
+
export async function checkBackendHealth(config, { fetchImpl = fetch, timeoutMs = 3_000 } = {}) {
|
|
16
|
+
const lines = [];
|
|
17
|
+
if (config.search.provider === 'duckduckgo') {
|
|
18
|
+
lines.push('search backend: duckduckgo');
|
|
19
|
+
}
|
|
20
|
+
else if (!config.search.baseUrl) {
|
|
21
|
+
lines.push('search backend: searxng warning (missing baseUrl)');
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
const timeout = withTimeout(timeoutMs);
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetchImpl(searxngDoctorUrl(config.search.baseUrl), { signal: timeout.signal });
|
|
27
|
+
const json = (await response.json());
|
|
28
|
+
lines.push(response.ok && Array.isArray(json.results)
|
|
29
|
+
? 'search backend: searxng ok'
|
|
30
|
+
: 'search backend: searxng warning (unexpected response)');
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
lines.push(`search backend: searxng warning (${message(error)})`);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
timeout.done();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (config.fetch.provider === 'http') {
|
|
40
|
+
lines.push('fetch backend: http');
|
|
41
|
+
}
|
|
42
|
+
else if (!config.fetch.baseUrl) {
|
|
43
|
+
lines.push('fetch backend: firecrawl warning (missing baseUrl)');
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const timeout = withTimeout(timeoutMs);
|
|
47
|
+
try {
|
|
48
|
+
const headers = { 'content-type': 'application/json' };
|
|
49
|
+
if (config.fetch.apiKey ?? process.env.PI_WEB_AGENT_FIRECRAWL_API_KEY) {
|
|
50
|
+
headers.Authorization = `Bearer ${config.fetch.apiKey ?? process.env.PI_WEB_AGENT_FIRECRAWL_API_KEY}`;
|
|
51
|
+
}
|
|
52
|
+
const response = await fetchImpl(new URL('/v1/scrape', config.fetch.baseUrl).toString(), {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers,
|
|
55
|
+
body: JSON.stringify({ url: 'https://example.com', formats: ['markdown'] }),
|
|
56
|
+
signal: timeout.signal
|
|
57
|
+
});
|
|
58
|
+
lines.push(response.ok ? 'fetch backend: firecrawl ok' : `fetch backend: firecrawl warning (HTTP ${response.status})`);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
lines.push(`fetch backend: firecrawl warning (${message(error)})`);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
timeout.done();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return lines;
|
|
68
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { WebFetchHeadlessResponse, WebFetchResponse, WebSearchResponse } from '../types.js';
|
|
2
|
+
import { type BackendConfig } from './config.js';
|
|
3
|
+
export type BackendSet = {
|
|
4
|
+
search: (input: {
|
|
5
|
+
query: string;
|
|
6
|
+
}) => Promise<WebSearchResponse>;
|
|
7
|
+
fetchPage: (input: {
|
|
8
|
+
url: string;
|
|
9
|
+
}) => Promise<WebFetchResponse>;
|
|
10
|
+
headlessFetch: (input: {
|
|
11
|
+
url: string;
|
|
12
|
+
}) => Promise<WebFetchHeadlessResponse>;
|
|
13
|
+
};
|
|
14
|
+
export declare function createBackendSet(config?: BackendConfig): BackendSet;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createFirecrawlFetcher } from '../fetch/firecrawl-fetch.js';
|
|
2
|
+
import { createSearxngSearchTool } from '../search/searxng.js';
|
|
3
|
+
import { buildFetchPresentation } from '../presentation/fetch-presentation.js';
|
|
4
|
+
import { buildSearchPresentation } from '../presentation/search-presentation.js';
|
|
5
|
+
import { createWebFetchHeadlessTool } from '../tools/web-fetch-headless.js';
|
|
6
|
+
import { createWebFetchTool } from '../tools/web-fetch.js';
|
|
7
|
+
import { createWebSearchTool } from '../tools/web-search.js';
|
|
8
|
+
import { DEFAULT_BACKEND_CONFIG } from './config.js';
|
|
9
|
+
function invalidSearxngSearch() {
|
|
10
|
+
return async function search() {
|
|
11
|
+
const result = {
|
|
12
|
+
status: 'error',
|
|
13
|
+
results: [],
|
|
14
|
+
metadata: { backend: 'searxng', cacheHit: false },
|
|
15
|
+
error: {
|
|
16
|
+
code: 'BACKEND_CONFIG_INVALID',
|
|
17
|
+
message: 'SearXNG search requires backends.search.baseUrl.'
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
return { ...result, presentation: buildSearchPresentation(result) };
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function invalidFirecrawlFetch() {
|
|
24
|
+
return async function fetchPage(url) {
|
|
25
|
+
const result = {
|
|
26
|
+
status: 'error',
|
|
27
|
+
url,
|
|
28
|
+
metadata: { method: 'firecrawl', cacheHit: false },
|
|
29
|
+
error: {
|
|
30
|
+
code: 'BACKEND_CONFIG_INVALID',
|
|
31
|
+
message: 'Firecrawl fetch requires backends.fetch.baseUrl.'
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
return { ...result, presentation: buildFetchPresentation(result) };
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function createBackendSet(config = DEFAULT_BACKEND_CONFIG) {
|
|
38
|
+
const search = config.search.provider === 'searxng'
|
|
39
|
+
? config.search.baseUrl
|
|
40
|
+
? createSearxngSearchTool({ baseUrl: config.search.baseUrl })
|
|
41
|
+
: invalidSearxngSearch()
|
|
42
|
+
: createWebSearchTool();
|
|
43
|
+
const fetchPage = config.fetch.provider === 'firecrawl'
|
|
44
|
+
? config.fetch.baseUrl
|
|
45
|
+
? createWebFetchTool({
|
|
46
|
+
fetchPage: createFirecrawlFetcher({
|
|
47
|
+
baseUrl: config.fetch.baseUrl,
|
|
48
|
+
apiKey: config.fetch.apiKey ?? process.env.PI_WEB_AGENT_FIRECRAWL_API_KEY
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
: createWebFetchTool({ fetchPage: invalidFirecrawlFetch() })
|
|
52
|
+
: createWebFetchTool();
|
|
53
|
+
return {
|
|
54
|
+
search,
|
|
55
|
+
fetchPage,
|
|
56
|
+
headlessFetch: createWebFetchHeadlessTool()
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -1,10 +1,20 @@
|
|
|
1
|
+
import { type BackendConfig } from '../backends/config.js';
|
|
1
2
|
import { type ExtensionAPI } from '@mariozechner/pi-coding-agent';
|
|
2
3
|
import { loadPresentationConfigLayers, type LoadedPresentationConfig } from '../presentation/config-store.js';
|
|
4
|
+
import { type BrowserResolutionResult } from '../fetch/browser-resolution.js';
|
|
3
5
|
import type { PresentationConfig, PresentationConfigOverride, PresentationScope } from '../presentation/types.js';
|
|
4
6
|
type CommandDeps = {
|
|
5
7
|
load?: () => ReturnType<typeof loadPresentationConfigLayers>;
|
|
6
8
|
save?: (scope: PresentationScope, config: PresentationConfigOverride) => Promise<void>;
|
|
7
9
|
reset?: (scope: PresentationScope) => Promise<void>;
|
|
10
|
+
resolveBrowser?: () => Promise<BrowserResolutionResult>;
|
|
11
|
+
runtime?: {
|
|
12
|
+
nodeVersion: string;
|
|
13
|
+
platform: string;
|
|
14
|
+
arch: string;
|
|
15
|
+
};
|
|
16
|
+
checkTypebox?: () => Promise<boolean>;
|
|
17
|
+
checkBackends?: (config: BackendConfig) => Promise<string[]>;
|
|
8
18
|
};
|
|
9
19
|
export type SettingsDraftState = {
|
|
10
20
|
scope: PresentationScope;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { DEFAULT_BACKEND_CONFIG, validateBackendConfig } from '../backends/config.js';
|
|
2
|
+
import { checkBackendHealth } from '../backends/doctor.js';
|
|
3
|
+
import { DynamicBorder, getSettingsListTheme } from '@mariozechner/pi-coding-agent';
|
|
4
|
+
import { Container, SelectList, SettingsList, Text } from '@mariozechner/pi-tui';
|
|
3
5
|
import { DEFAULT_PRESENTATION_CONFIG, mergePresentationConfigLayers, resolvePresentationMode } from '../presentation/config.js';
|
|
4
6
|
import { loadPresentationConfigLayers, resetPresentationConfigScope, savePresentationConfigScope } from '../presentation/config-store.js';
|
|
7
|
+
import { resolveBrowserExecutable } from '../fetch/browser-resolution.js';
|
|
5
8
|
const PRESENTATION_TOOL_NAMES = ['web_explore'];
|
|
6
9
|
function parseScopeToken(token) {
|
|
7
10
|
return token === 'global' || token === 'project' ? token : undefined;
|
|
@@ -12,6 +15,28 @@ function clonePresentationConfig(config) {
|
|
|
12
15
|
tools: { ...config.tools }
|
|
13
16
|
};
|
|
14
17
|
}
|
|
18
|
+
async function defaultCheckTypebox() {
|
|
19
|
+
try {
|
|
20
|
+
await import('typebox');
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function formatBackendSummary(config = DEFAULT_BACKEND_CONFIG) {
|
|
28
|
+
const search = config.search.baseUrl
|
|
29
|
+
? `search: ${config.search.provider} (${config.search.baseUrl})`
|
|
30
|
+
: `search: ${config.search.provider}`;
|
|
31
|
+
const fetch = config.fetch.baseUrl
|
|
32
|
+
? `fetch: ${config.fetch.provider} (${config.fetch.baseUrl})`
|
|
33
|
+
: `fetch: ${config.fetch.provider}`;
|
|
34
|
+
return [
|
|
35
|
+
search,
|
|
36
|
+
fetch,
|
|
37
|
+
`headless: ${config.headless.provider}`
|
|
38
|
+
].join('\n');
|
|
39
|
+
}
|
|
15
40
|
function formatConfigSummary(config) {
|
|
16
41
|
const lines = [`defaultMode: ${config.defaultMode}`];
|
|
17
42
|
for (const toolName of PRESENTATION_TOOL_NAMES) {
|
|
@@ -137,6 +162,40 @@ export function handleSettingsShortcut(data) {
|
|
|
137
162
|
}
|
|
138
163
|
return undefined;
|
|
139
164
|
}
|
|
165
|
+
async function openActionMenu(ctx) {
|
|
166
|
+
return ctx.ui.custom((tui, theme, _kb, done) => {
|
|
167
|
+
const container = new Container();
|
|
168
|
+
const items = [
|
|
169
|
+
{ value: 'settings', label: 'Settings', description: 'Edit presentation modes' },
|
|
170
|
+
{ value: 'show', label: 'Show config', description: 'Print effective config paths and modes' },
|
|
171
|
+
{ value: 'doctor', label: 'Doctor', description: 'Check runtime dependencies and browser detection' },
|
|
172
|
+
{ value: 'reset-project', label: 'Reset project config', description: 'Delete project-level overrides' },
|
|
173
|
+
{ value: 'reset-global', label: 'Reset global config', description: 'Delete global overrides' }
|
|
174
|
+
];
|
|
175
|
+
container.addChild(new DynamicBorder((text) => theme.fg('accent', text)));
|
|
176
|
+
container.addChild(new Text(theme.fg('accent', theme.bold('pi-web-agent')), 1, 0));
|
|
177
|
+
const list = new SelectList(items, Math.min(items.length, 8), {
|
|
178
|
+
selectedPrefix: (text) => theme.fg('accent', text),
|
|
179
|
+
selectedText: (text) => theme.fg('accent', text),
|
|
180
|
+
description: (text) => theme.fg('muted', text),
|
|
181
|
+
scrollInfo: (text) => theme.fg('dim', text),
|
|
182
|
+
noMatch: (text) => theme.fg('warning', text)
|
|
183
|
+
});
|
|
184
|
+
list.onSelect = (item) => done(item.value);
|
|
185
|
+
list.onCancel = () => done(undefined);
|
|
186
|
+
container.addChild(list);
|
|
187
|
+
container.addChild(new Text(theme.fg('dim', '↑↓ navigate • enter select • esc cancel'), 1, 0));
|
|
188
|
+
container.addChild(new DynamicBorder((text) => theme.fg('accent', text)));
|
|
189
|
+
return {
|
|
190
|
+
render: (width) => container.render(width),
|
|
191
|
+
invalidate: () => container.invalidate(),
|
|
192
|
+
handleInput: (data) => {
|
|
193
|
+
list.handleInput?.(data);
|
|
194
|
+
tui.requestRender?.();
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
}
|
|
140
199
|
async function openSettingsUi(ctx, loaded, initialScope) {
|
|
141
200
|
return ctx.ui.custom((_tui, theme, _kb, done) => {
|
|
142
201
|
let state = createSettingsDraftState(loaded, initialScope);
|
|
@@ -182,14 +241,63 @@ export function registerWebAgentConfigCommands(pi, deps = {}) {
|
|
|
182
241
|
const load = deps.load ?? (() => loadPresentationConfigLayers());
|
|
183
242
|
const save = deps.save ?? ((scope, config) => savePresentationConfigScope({}, scope, config));
|
|
184
243
|
const reset = deps.reset ?? ((scope) => resetPresentationConfigScope({}, scope));
|
|
244
|
+
const resolveBrowser = deps.resolveBrowser ?? (() => resolveBrowserExecutable({}));
|
|
245
|
+
const runtime = deps.runtime ?? {
|
|
246
|
+
nodeVersion: process.version,
|
|
247
|
+
platform: process.platform,
|
|
248
|
+
arch: process.arch
|
|
249
|
+
};
|
|
250
|
+
const checkTypebox = deps.checkTypebox ?? defaultCheckTypebox;
|
|
251
|
+
const checkBackends = deps.checkBackends ?? ((config) => checkBackendHealth(config));
|
|
185
252
|
pi.registerCommand('web-agent', {
|
|
186
253
|
description: 'Open settings or manage pi-web-agent presentation config',
|
|
187
254
|
handler: async (args, ctx) => {
|
|
188
|
-
|
|
255
|
+
let [action, maybeScope] = (args ?? '').trim().split(/\s+/).filter(Boolean);
|
|
256
|
+
if (!action) {
|
|
257
|
+
const selectedAction = await openActionMenu(ctx);
|
|
258
|
+
if (!selectedAction)
|
|
259
|
+
return;
|
|
260
|
+
if (selectedAction === 'reset-project') {
|
|
261
|
+
action = 'reset';
|
|
262
|
+
maybeScope = 'project';
|
|
263
|
+
}
|
|
264
|
+
else if (selectedAction === 'reset-global') {
|
|
265
|
+
action = 'reset';
|
|
266
|
+
maybeScope = 'global';
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
action = selectedAction;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (action === 'doctor') {
|
|
273
|
+
const [typeboxOk, browser, loaded] = await Promise.all([checkTypebox(), resolveBrowser(), load()]);
|
|
274
|
+
const backendConfig = loaded.effectiveBackends ?? DEFAULT_BACKEND_CONFIG;
|
|
275
|
+
const backendIssues = validateBackendConfig(backendConfig);
|
|
276
|
+
const backendHealth = await checkBackends(backendConfig);
|
|
277
|
+
const lines = [
|
|
278
|
+
'pi-web-agent: loaded',
|
|
279
|
+
`runtime: node ${runtime.nodeVersion} ${runtime.platform} ${runtime.arch}`,
|
|
280
|
+
`typebox: ${typeboxOk ? 'ok' : 'missing'}`,
|
|
281
|
+
formatBackendSummary(backendConfig),
|
|
282
|
+
backendIssues.length > 0 ? `backend config: warning\n${backendIssues.join('\n')}` : 'backend config: ok',
|
|
283
|
+
...backendHealth
|
|
284
|
+
];
|
|
285
|
+
if (browser.ok) {
|
|
286
|
+
lines.push(`browser: ${browser.browser} ${browser.executablePath}`);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
lines.push(`browser: missing (${browser.error.code})`);
|
|
290
|
+
lines.push(browser.error.message);
|
|
291
|
+
lines.push('Install Chrome, Chromium, Edge, or Brave and run /web-agent doctor again.');
|
|
292
|
+
}
|
|
293
|
+
ctx.ui.notify(lines.join('\n'), 'info');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
189
296
|
if (action === 'show') {
|
|
190
297
|
const loaded = await load();
|
|
191
298
|
ctx.ui.notify([
|
|
192
299
|
formatConfigSummary(loaded.effectiveConfig),
|
|
300
|
+
formatBackendSummary(loaded.effectiveBackends ?? DEFAULT_BACKEND_CONFIG),
|
|
193
301
|
`global: ${loaded.global.path}${loaded.global.exists ? '' : ' (missing)'}`,
|
|
194
302
|
`project: ${loaded.project.path}${loaded.project.exists ? '' : ' (missing)'}`
|
|
195
303
|
].join('\n'), 'info');
|
|
@@ -243,7 +351,7 @@ export function registerWebAgentConfigCommands(pi, deps = {}) {
|
|
|
243
351
|
ctx.ui.notify(`Saved ${result.scope} config`, 'info');
|
|
244
352
|
return;
|
|
245
353
|
}
|
|
246
|
-
ctx.ui.notify('Use /web-agent, /web-agent show, /web-agent reset project, or /web-agent settings', 'info');
|
|
354
|
+
ctx.ui.notify('Use /web-agent, /web-agent show, /web-agent doctor, /web-agent reset project, or /web-agent settings', 'info');
|
|
247
355
|
}
|
|
248
356
|
});
|
|
249
357
|
}
|
package/dist/extension.js
CHANGED
|
@@ -1,19 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DEFAULT_BACKEND_CONFIG } from './backends/config.js';
|
|
2
|
+
import { Type } from 'typebox';
|
|
2
3
|
import { registerWebAgentConfigCommands } from './commands/web-agent-config.js';
|
|
3
4
|
import { DEFAULT_PRESENTATION_CONFIG, resolvePresentationMode } from './presentation/config.js';
|
|
5
|
+
import { createResearchWorkflow } from './orchestration/index.js';
|
|
4
6
|
import { loadPresentationConfigLayers } from './presentation/config-store.js';
|
|
5
7
|
import { selectPresentationView } from './presentation/select-view.js';
|
|
6
8
|
import { createWebExploreTool } from './tools/web-explore.js';
|
|
7
|
-
async function
|
|
9
|
+
async function loadWebAgentConfig(pi) {
|
|
8
10
|
const store = pi.__presentationConfigStore;
|
|
11
|
+
return store?.load?.() ?? loadPresentationConfigLayers();
|
|
12
|
+
}
|
|
13
|
+
async function getEffectivePresentationConfig(pi) {
|
|
9
14
|
try {
|
|
10
|
-
const loaded = await (
|
|
15
|
+
const loaded = await loadWebAgentConfig(pi);
|
|
11
16
|
return loaded.effectiveConfig;
|
|
12
17
|
}
|
|
13
18
|
catch {
|
|
14
19
|
return DEFAULT_PRESENTATION_CONFIG;
|
|
15
20
|
}
|
|
16
21
|
}
|
|
22
|
+
async function getEffectiveBackendConfig(pi) {
|
|
23
|
+
try {
|
|
24
|
+
const loaded = await loadWebAgentConfig(pi);
|
|
25
|
+
return loaded.effectiveBackends ?? DEFAULT_BACKEND_CONFIG;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return DEFAULT_BACKEND_CONFIG;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
17
31
|
async function renderToolText(pi, toolName, details) {
|
|
18
32
|
const config = await getEffectivePresentationConfig(pi);
|
|
19
33
|
const mode = resolvePresentationMode(toolName, config);
|
|
@@ -21,8 +35,22 @@ async function renderToolText(pi, toolName, details) {
|
|
|
21
35
|
}
|
|
22
36
|
export default function extension(pi) {
|
|
23
37
|
registerWebAgentConfigCommands(pi);
|
|
24
|
-
const
|
|
25
|
-
|
|
38
|
+
const injectedWebExplore = pi.__webExploreTool;
|
|
39
|
+
let cachedBackendKey;
|
|
40
|
+
let cachedWebExplore;
|
|
41
|
+
async function getConfiguredWebExplore() {
|
|
42
|
+
if (injectedWebExplore)
|
|
43
|
+
return injectedWebExplore;
|
|
44
|
+
const backendConfig = await getEffectiveBackendConfig(pi);
|
|
45
|
+
const backendKey = JSON.stringify(backendConfig);
|
|
46
|
+
if (!cachedWebExplore || cachedBackendKey !== backendKey) {
|
|
47
|
+
cachedBackendKey = backendKey;
|
|
48
|
+
cachedWebExplore = createWebExploreTool({
|
|
49
|
+
explore: createResearchWorkflow({ backendConfig })
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return cachedWebExplore;
|
|
53
|
+
}
|
|
26
54
|
pi.on('before_agent_start', async (event) => ({
|
|
27
55
|
systemPrompt: `${event.systemPrompt}\n\n` +
|
|
28
56
|
'For web research questions that require finding and comparing sources, use web_explore. ' +
|
|
@@ -37,6 +65,7 @@ export default function extension(pi) {
|
|
|
37
65
|
query: Type.String({ description: 'Web research question to explore.' })
|
|
38
66
|
}),
|
|
39
67
|
async execute(_toolCallId, params) {
|
|
68
|
+
const webExplore = await getConfiguredWebExplore();
|
|
40
69
|
const result = await webExplore({ query: params.query });
|
|
41
70
|
return {
|
|
42
71
|
content: [
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
type SupportedPlatform = NodeJS.Platform | string;
|
|
2
|
+
type BrowserName = 'configured' | 'chrome' | 'edge' | 'brave' | 'chromium';
|
|
1
3
|
export type BrowserResolutionResult = {
|
|
2
4
|
ok: true;
|
|
3
5
|
executablePath: string;
|
|
4
|
-
browser:
|
|
6
|
+
browser: BrowserName;
|
|
5
7
|
} | {
|
|
6
8
|
ok: false;
|
|
7
9
|
error: {
|
|
@@ -9,7 +11,10 @@ export type BrowserResolutionResult = {
|
|
|
9
11
|
message: string;
|
|
10
12
|
};
|
|
11
13
|
};
|
|
12
|
-
export declare function resolveBrowserExecutable({ configuredPath, fileExists }: {
|
|
14
|
+
export declare function resolveBrowserExecutable({ configuredPath, platform, env, fileExists }: {
|
|
13
15
|
configuredPath?: string;
|
|
16
|
+
platform?: SupportedPlatform;
|
|
17
|
+
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
14
18
|
fileExists?: (path: string) => Promise<boolean>;
|
|
15
19
|
}): Promise<BrowserResolutionResult>;
|
|
20
|
+
export {};
|
|
@@ -1,14 +1,82 @@
|
|
|
1
|
-
const WINDOWS_CANDIDATES =
|
|
2
|
-
|
|
3
|
-
'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
const WINDOWS_CANDIDATES = [
|
|
2
|
+
{
|
|
3
|
+
browser: 'chrome',
|
|
4
|
+
paths: [
|
|
5
|
+
'C:/Program Files/Google/Chrome/Application/chrome.exe',
|
|
6
|
+
'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe'
|
|
7
|
+
],
|
|
8
|
+
commands: ['chrome.exe', 'chrome', 'google-chrome']
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
browser: 'edge',
|
|
12
|
+
paths: [
|
|
13
|
+
'C:/Program Files/Microsoft/Edge/Application/msedge.exe',
|
|
14
|
+
'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'
|
|
15
|
+
],
|
|
16
|
+
commands: ['msedge.exe', 'msedge', 'microsoft-edge']
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
browser: 'brave',
|
|
20
|
+
paths: [
|
|
21
|
+
'C:/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe',
|
|
22
|
+
'C:/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe'
|
|
23
|
+
],
|
|
24
|
+
commands: ['brave.exe', 'brave']
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
browser: 'chromium',
|
|
28
|
+
paths: [
|
|
29
|
+
'C:/Program Files/Chromium/Application/chrome.exe',
|
|
30
|
+
'C:/Program Files (x86)/Chromium/Application/chrome.exe'
|
|
31
|
+
],
|
|
32
|
+
commands: ['chromium.exe', 'chromium']
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
const MACOS_CANDIDATES = [
|
|
36
|
+
{
|
|
37
|
+
browser: 'chrome',
|
|
38
|
+
paths: ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'],
|
|
39
|
+
commands: ['google-chrome', 'chrome']
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
browser: 'edge',
|
|
43
|
+
paths: ['/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'],
|
|
44
|
+
commands: ['microsoft-edge', 'msedge']
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
browser: 'brave',
|
|
48
|
+
paths: ['/Applications/Brave Browser.app/Contents/MacOS/Brave Browser'],
|
|
49
|
+
commands: ['brave-browser', 'brave']
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
browser: 'chromium',
|
|
53
|
+
paths: ['/Applications/Chromium.app/Contents/MacOS/Chromium'],
|
|
54
|
+
commands: ['chromium', 'chromium-browser']
|
|
55
|
+
}
|
|
56
|
+
];
|
|
57
|
+
const LINUX_CANDIDATES = [
|
|
58
|
+
{
|
|
59
|
+
browser: 'chrome',
|
|
60
|
+
paths: ['/usr/bin/google-chrome', '/usr/local/bin/google-chrome', '/opt/google/chrome/chrome'],
|
|
61
|
+
commands: ['google-chrome', 'google-chrome-stable', 'chrome']
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
browser: 'edge',
|
|
65
|
+
paths: ['/usr/bin/microsoft-edge', '/opt/microsoft/msedge/msedge'],
|
|
66
|
+
commands: ['microsoft-edge', 'microsoft-edge-stable', 'msedge']
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
browser: 'brave',
|
|
70
|
+
paths: ['/usr/bin/brave-browser', '/usr/local/bin/brave-browser', '/opt/brave.com/brave/brave'],
|
|
71
|
+
commands: ['brave-browser', 'brave']
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
browser: 'chromium',
|
|
75
|
+
paths: ['/usr/bin/chromium', '/usr/bin/chromium-browser', '/snap/bin/chromium'],
|
|
76
|
+
commands: ['chromium', 'chromium-browser']
|
|
77
|
+
}
|
|
78
|
+
];
|
|
79
|
+
export async function resolveBrowserExecutable({ configuredPath, platform = process.platform, env = process.env, fileExists = defaultFileExists }) {
|
|
12
80
|
if (configuredPath) {
|
|
13
81
|
if (await fileExists(configuredPath)) {
|
|
14
82
|
return {
|
|
@@ -25,14 +93,19 @@ export async function resolveBrowserExecutable({ configuredPath, fileExists = de
|
|
|
25
93
|
}
|
|
26
94
|
};
|
|
27
95
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
96
|
+
const candidates = getCandidatesForPlatform(platform);
|
|
97
|
+
for (const candidate of candidates) {
|
|
98
|
+
for (const path of candidate.paths) {
|
|
99
|
+
if (await fileExists(path)) {
|
|
100
|
+
return { ok: true, executablePath: path, browser: candidate.browser };
|
|
101
|
+
}
|
|
31
102
|
}
|
|
32
103
|
}
|
|
33
|
-
for (const
|
|
34
|
-
|
|
35
|
-
|
|
104
|
+
for (const candidate of candidates) {
|
|
105
|
+
for (const path of getPathCommandCandidates(candidate.commands, platform, env)) {
|
|
106
|
+
if (await fileExists(path)) {
|
|
107
|
+
return { ok: true, executablePath: path, browser: candidate.browser };
|
|
108
|
+
}
|
|
36
109
|
}
|
|
37
110
|
}
|
|
38
111
|
return {
|
|
@@ -43,6 +116,27 @@ export async function resolveBrowserExecutable({ configuredPath, fileExists = de
|
|
|
43
116
|
}
|
|
44
117
|
};
|
|
45
118
|
}
|
|
119
|
+
function getCandidatesForPlatform(platform) {
|
|
120
|
+
if (platform === 'win32')
|
|
121
|
+
return WINDOWS_CANDIDATES;
|
|
122
|
+
if (platform === 'darwin')
|
|
123
|
+
return MACOS_CANDIDATES;
|
|
124
|
+
if (platform === 'linux')
|
|
125
|
+
return LINUX_CANDIDATES;
|
|
126
|
+
return [...WINDOWS_CANDIDATES, ...MACOS_CANDIDATES, ...LINUX_CANDIDATES];
|
|
127
|
+
}
|
|
128
|
+
function getPathCommandCandidates(commands, platform, env) {
|
|
129
|
+
const pathValue = env.PATH ?? env.Path ?? env.path;
|
|
130
|
+
if (!pathValue)
|
|
131
|
+
return [];
|
|
132
|
+
const delimiter = platform === 'win32' ? ';' : ':';
|
|
133
|
+
const dirs = pathValue.split(delimiter).filter(Boolean);
|
|
134
|
+
const extensions = platform === 'win32' ? ['', '.exe'] : [''];
|
|
135
|
+
return dirs.flatMap((dir) => commands.flatMap((command) => extensions.map((extension) => {
|
|
136
|
+
const normalizedCommand = command.toLowerCase().endsWith(extension) ? command : `${command}${extension}`;
|
|
137
|
+
return `${dir.replace(/[\\/]$/, '')}/${normalizedCommand}`;
|
|
138
|
+
})));
|
|
139
|
+
}
|
|
46
140
|
async function defaultFileExists(path) {
|
|
47
141
|
try {
|
|
48
142
|
const { access } = await import('node:fs/promises');
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
function buildScrapeUrl(baseUrl) {
|
|
2
|
+
return new URL('/v1/scrape', baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`).toString();
|
|
3
|
+
}
|
|
4
|
+
function errorMessage(error) {
|
|
5
|
+
return error instanceof Error ? error.message : String(error);
|
|
6
|
+
}
|
|
7
|
+
export function createFirecrawlFetcher({ baseUrl, apiKey, fetchImpl = fetch }) {
|
|
8
|
+
return async function firecrawlFetch(url) {
|
|
9
|
+
try {
|
|
10
|
+
const headers = { 'content-type': 'application/json' };
|
|
11
|
+
if (apiKey) {
|
|
12
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
13
|
+
}
|
|
14
|
+
const response = await fetchImpl(buildScrapeUrl(baseUrl), {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers,
|
|
17
|
+
body: JSON.stringify({ url, formats: ['markdown'] })
|
|
18
|
+
});
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
throw new Error(`HTTP ${response.status}`);
|
|
21
|
+
}
|
|
22
|
+
const parsed = (await response.json());
|
|
23
|
+
if (parsed.success === false) {
|
|
24
|
+
throw new Error(typeof parsed.error === 'string' ? parsed.error : 'Firecrawl scrape failed.');
|
|
25
|
+
}
|
|
26
|
+
const text = typeof parsed.data?.markdown === 'string'
|
|
27
|
+
? parsed.data.markdown
|
|
28
|
+
: typeof parsed.data?.html === 'string'
|
|
29
|
+
? parsed.data.html
|
|
30
|
+
: '';
|
|
31
|
+
const resolvedUrl = typeof parsed.data?.metadata?.sourceURL === 'string'
|
|
32
|
+
? parsed.data.metadata.sourceURL
|
|
33
|
+
: url;
|
|
34
|
+
const title = typeof parsed.data?.metadata?.title === 'string'
|
|
35
|
+
? parsed.data.metadata.title
|
|
36
|
+
: undefined;
|
|
37
|
+
if (!text.trim()) {
|
|
38
|
+
return {
|
|
39
|
+
status: 'needs_headless',
|
|
40
|
+
url: resolvedUrl,
|
|
41
|
+
metadata: { method: 'firecrawl', cacheHit: false },
|
|
42
|
+
error: { code: 'WEAK_EXTRACTION', message: 'Firecrawl did not return useful page text.' }
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
status: 'ok',
|
|
47
|
+
url: resolvedUrl,
|
|
48
|
+
content: { title, text },
|
|
49
|
+
metadata: { method: 'firecrawl', cacheHit: false, truncated: text.length >= 4000 }
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
return {
|
|
54
|
+
status: 'error',
|
|
55
|
+
url,
|
|
56
|
+
metadata: { method: 'firecrawl', cacheHit: false },
|
|
57
|
+
error: { code: 'FETCH_FAILED', message: `Firecrawl scrape failed: ${errorMessage(error)}` }
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { BackendConfig } from '../backends/config.js';
|
|
1
2
|
import type { WebFetchHeadlessResponse, WebFetchResponse, WebSearchResponse } from '../types.js';
|
|
2
|
-
export declare function createResearchWorkflow({ search, fetchPage, headlessFetch }?: {
|
|
3
|
+
export declare function createResearchWorkflow({ backendConfig, search, fetchPage, headlessFetch }?: {
|
|
4
|
+
backendConfig?: BackendConfig;
|
|
3
5
|
search?: (input: {
|
|
4
6
|
query: string;
|
|
5
7
|
}) => Promise<WebSearchResponse>;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createWebFetchTool } from '../tools/web-fetch.js';
|
|
3
|
-
import { createWebSearchTool } from '../tools/web-search.js';
|
|
1
|
+
import { createBackendSet } from '../backends/factory.js';
|
|
4
2
|
import { createResearchOrchestrator } from './research-orchestrator.js';
|
|
5
3
|
import { createResearchWorker } from './research-worker.js';
|
|
6
|
-
export function createResearchWorkflow({ search
|
|
7
|
-
const
|
|
8
|
-
|
|
4
|
+
export function createResearchWorkflow({ backendConfig, search, fetchPage, headlessFetch } = {}) {
|
|
5
|
+
const backends = createBackendSet(backendConfig);
|
|
6
|
+
const resolvedSearch = search ?? backends.search;
|
|
7
|
+
const resolvedFetchPage = fetchPage ?? backends.fetchPage;
|
|
8
|
+
const resolvedHeadlessFetch = headlessFetch ?? backends.headlessFetch;
|
|
9
|
+
const worker = createResearchWorker({ search: resolvedSearch, fetchPage: resolvedFetchPage });
|
|
10
|
+
return createResearchOrchestrator({ worker, headlessFetch: resolvedHeadlessFetch });
|
|
9
11
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type ResearchSourceKind = 'official-docs' | 'official-api' | 'official-discussion' | 'community' | 'issue-thread' | 'package-page' | 'other';
|
|
2
|
-
export type ResearchMethod = 'search' | 'http' | 'headless';
|
|
2
|
+
export type ResearchMethod = 'search' | 'http' | 'headless' | 'firecrawl';
|
|
3
3
|
export type ResearchEvidence = {
|
|
4
4
|
title: string;
|
|
5
5
|
url: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type BackendConfig, type BackendConfigOverride } from '../backends/config.js';
|
|
1
2
|
import type { PresentationConfig, PresentationConfigOverride, PresentationScope } from './types.js';
|
|
2
3
|
export type PresentationConfigStoreOptions = {
|
|
3
4
|
homeDir?: string;
|
|
@@ -7,12 +8,14 @@ export type PresentationConfigLayer = {
|
|
|
7
8
|
path: string;
|
|
8
9
|
exists: boolean;
|
|
9
10
|
rawConfig?: PresentationConfigOverride;
|
|
11
|
+
rawBackends?: BackendConfigOverride;
|
|
10
12
|
error?: string;
|
|
11
13
|
};
|
|
12
14
|
export type LoadedPresentationConfig = {
|
|
13
15
|
global: PresentationConfigLayer;
|
|
14
16
|
project: PresentationConfigLayer;
|
|
15
17
|
effectiveConfig: PresentationConfig;
|
|
18
|
+
effectiveBackends: BackendConfig;
|
|
16
19
|
};
|
|
17
20
|
export declare function getPresentationConfigPaths(options?: PresentationConfigStoreOptions): {
|
|
18
21
|
globalPath: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { DEFAULT_BACKEND_CONFIG, extractBackendConfigOverride, mergeBackendConfigLayers } from '../backends/config.js';
|
|
3
4
|
import { DEFAULT_PRESENTATION_CONFIG, extractPresentationConfigOverride, mergePresentationConfigLayers } from './config.js';
|
|
4
5
|
export function getPresentationConfigPaths(options = {}) {
|
|
5
6
|
const homeDir = options.homeDir ?? process.env.USERPROFILE ?? process.env.HOME ?? '';
|
|
@@ -9,14 +10,21 @@ export function getPresentationConfigPaths(options = {}) {
|
|
|
9
10
|
projectPath: path.join(projectDir, '.pi', 'extensions', 'pi-web-agent', 'config.json')
|
|
10
11
|
};
|
|
11
12
|
}
|
|
13
|
+
function hasPresentationRoot(parsed) {
|
|
14
|
+
return parsed.presentation !== undefined;
|
|
15
|
+
}
|
|
12
16
|
async function readPresentationConfigFile(filePath) {
|
|
13
17
|
try {
|
|
14
18
|
const rawText = await readFile(filePath, 'utf8');
|
|
15
19
|
const parsed = JSON.parse(rawText);
|
|
20
|
+
const presentationFile = hasPresentationRoot(parsed)
|
|
21
|
+
? parsed
|
|
22
|
+
: { presentation: parsed };
|
|
16
23
|
return {
|
|
17
24
|
path: filePath,
|
|
18
25
|
exists: true,
|
|
19
|
-
rawConfig: extractPresentationConfigOverride(
|
|
26
|
+
rawConfig: extractPresentationConfigOverride(presentationFile),
|
|
27
|
+
rawBackends: extractBackendConfigOverride(parsed)
|
|
20
28
|
};
|
|
21
29
|
}
|
|
22
30
|
catch (error) {
|
|
@@ -48,7 +56,8 @@ export async function loadPresentationConfigLayers(options = {}) {
|
|
|
48
56
|
return {
|
|
49
57
|
global,
|
|
50
58
|
project,
|
|
51
|
-
effectiveConfig: mergePresentationConfigLayers(DEFAULT_PRESENTATION_CONFIG, global.rawConfig, project.rawConfig)
|
|
59
|
+
effectiveConfig: mergePresentationConfigLayers(DEFAULT_PRESENTATION_CONFIG, global.rawConfig, project.rawConfig),
|
|
60
|
+
effectiveBackends: mergeBackendConfigLayers(DEFAULT_BACKEND_CONFIG, global.rawBackends, project.rawBackends)
|
|
52
61
|
};
|
|
53
62
|
}
|
|
54
63
|
export async function savePresentationConfigScope(options, scope, config) {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { buildSearchPresentation } from '../presentation/search-presentation.js';
|
|
2
|
+
function buildSearchUrl(baseUrl, query) {
|
|
3
|
+
const url = new URL('/search', baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);
|
|
4
|
+
url.searchParams.set('q', query);
|
|
5
|
+
url.searchParams.set('format', 'json');
|
|
6
|
+
return url.toString();
|
|
7
|
+
}
|
|
8
|
+
function normalizeResults(response) {
|
|
9
|
+
return (response.results ?? []).flatMap((result) => {
|
|
10
|
+
if (typeof result.title !== 'string' || typeof result.url !== 'string') {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
title: result.title,
|
|
16
|
+
url: result.url,
|
|
17
|
+
snippet: typeof result.content === 'string' ? result.content : ''
|
|
18
|
+
}
|
|
19
|
+
];
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function createSearxngSearchTool({ baseUrl, fetchImpl = fetch }) {
|
|
23
|
+
return async function searxngSearch({ query }) {
|
|
24
|
+
const normalizedQuery = query.trim();
|
|
25
|
+
if (!normalizedQuery) {
|
|
26
|
+
const result = {
|
|
27
|
+
status: 'error',
|
|
28
|
+
results: [],
|
|
29
|
+
metadata: { backend: 'searxng', cacheHit: false },
|
|
30
|
+
error: { code: 'INVALID_QUERY', message: 'Query must not be empty.' }
|
|
31
|
+
};
|
|
32
|
+
return { ...result, presentation: buildSearchPresentation(result) };
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetchImpl(buildSearchUrl(baseUrl, normalizedQuery));
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error(`HTTP ${response.status}`);
|
|
38
|
+
}
|
|
39
|
+
const parsed = (await response.json());
|
|
40
|
+
const results = normalizeResults(parsed);
|
|
41
|
+
const result = results.length > 0
|
|
42
|
+
? {
|
|
43
|
+
status: 'ok',
|
|
44
|
+
results,
|
|
45
|
+
metadata: { backend: 'searxng', cacheHit: false }
|
|
46
|
+
}
|
|
47
|
+
: {
|
|
48
|
+
status: 'error',
|
|
49
|
+
results: [],
|
|
50
|
+
metadata: { backend: 'searxng', cacheHit: false },
|
|
51
|
+
error: { code: 'NO_RESULTS', message: 'SearXNG returned no usable results for this query.' }
|
|
52
|
+
};
|
|
53
|
+
return { ...result, presentation: buildSearchPresentation(result) };
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
57
|
+
const result = {
|
|
58
|
+
status: 'error',
|
|
59
|
+
results: [],
|
|
60
|
+
metadata: { backend: 'searxng', cacheHit: false },
|
|
61
|
+
error: { code: 'FETCH_FAILED', message: `SearXNG search request failed: ${rawMessage}` }
|
|
62
|
+
};
|
|
63
|
+
return { ...result, presentation: buildSearchPresentation(result) };
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -11,15 +11,15 @@ export type ToolError = {
|
|
|
11
11
|
message: string;
|
|
12
12
|
};
|
|
13
13
|
export type SearchMetadata = {
|
|
14
|
-
backend: 'duckduckgo';
|
|
14
|
+
backend: 'duckduckgo' | 'searxng';
|
|
15
15
|
cacheHit: boolean;
|
|
16
16
|
};
|
|
17
17
|
export type FetchMetadata = {
|
|
18
|
-
method: 'http' | 'headless';
|
|
18
|
+
method: 'http' | 'headless' | 'firecrawl';
|
|
19
19
|
cacheHit: boolean;
|
|
20
20
|
contentType?: string;
|
|
21
21
|
truncated?: boolean;
|
|
22
|
-
browser?: 'configured' | 'chrome' | 'edge';
|
|
22
|
+
browser?: 'configured' | 'chrome' | 'edge' | 'brave' | 'chromium';
|
|
23
23
|
navigationMs?: number;
|
|
24
24
|
};
|
|
25
25
|
export type ExtractedContent = {
|
|
@@ -56,7 +56,7 @@ export type WebExploreResponse = {
|
|
|
56
56
|
sources: Array<{
|
|
57
57
|
title: string;
|
|
58
58
|
url: string;
|
|
59
|
-
method?: 'http' | 'headless';
|
|
59
|
+
method?: 'http' | 'headless' | 'firecrawl';
|
|
60
60
|
}>;
|
|
61
61
|
caveat?: string;
|
|
62
62
|
metadata?: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@demigodmode/pi-web-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -56,11 +56,11 @@
|
|
|
56
56
|
"@mozilla/readability": "^0.6.0",
|
|
57
57
|
"cheerio": "^1.1.0",
|
|
58
58
|
"jsdom": "^26.0.0",
|
|
59
|
-
"playwright-core": "^1.54.0"
|
|
59
|
+
"playwright-core": "^1.54.0",
|
|
60
|
+
"typebox": "^1.1.37"
|
|
60
61
|
},
|
|
61
62
|
"devDependencies": {
|
|
62
|
-
"@mariozechner/pi-coding-agent": "^0.
|
|
63
|
-
"@sinclair/typebox": "^0.34.41",
|
|
63
|
+
"@mariozechner/pi-coding-agent": "^0.69.0",
|
|
64
64
|
"@types/jsdom": "^21.1.7",
|
|
65
65
|
"@types/node": "^24.0.0",
|
|
66
66
|
"@vitest/coverage-v8": "^3.2.4",
|
|
@@ -69,7 +69,6 @@
|
|
|
69
69
|
"vitest": "^3.2.0"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
|
-
"@mariozechner/pi-coding-agent": "*"
|
|
73
|
-
"@sinclair/typebox": "*"
|
|
72
|
+
"@mariozechner/pi-coding-agent": "*"
|
|
74
73
|
}
|
|
75
74
|
}
|