@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 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,5 @@
1
+ import type { BackendConfig } from './config.js';
2
+ export declare function checkBackendHealth(config: BackendConfig, { fetchImpl, timeoutMs }?: {
3
+ fetchImpl?: typeof fetch;
4
+ timeoutMs?: number;
5
+ }): Promise<string[]>;
@@ -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 { getSettingsListTheme } from '@mariozechner/pi-coding-agent';
2
- import { Container, SettingsList, Text } from '@mariozechner/pi-tui';
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
- const [action, maybeScope] = (args ?? '').trim().split(/\s+/).filter(Boolean);
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 { Type } from '@sinclair/typebox';
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 getEffectivePresentationConfig(pi) {
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 (store?.load?.() ?? loadPresentationConfigLayers());
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 webExplore = pi.__webExploreTool ??
25
- createWebExploreTool();
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: 'configured' | 'chrome' | 'edge';
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
- chrome: [
3
- 'C:/Program Files/Google/Chrome/Application/chrome.exe',
4
- 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe'
5
- ],
6
- edge: [
7
- 'C:/Program Files/Microsoft/Edge/Application/msedge.exe',
8
- 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'
9
- ]
10
- };
11
- export async function resolveBrowserExecutable({ configuredPath, fileExists = defaultFileExists }) {
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
- for (const path of WINDOWS_CANDIDATES.chrome) {
29
- if (await fileExists(path)) {
30
- return { ok: true, executablePath: path, browser: 'chrome' };
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 path of WINDOWS_CANDIDATES.edge) {
34
- if (await fileExists(path)) {
35
- return { ok: true, executablePath: path, browser: 'edge' };
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,6 @@
1
+ import type { WebFetchResponse } from '../types.js';
2
+ export declare function createFirecrawlFetcher({ baseUrl, apiKey, fetchImpl }: {
3
+ baseUrl: string;
4
+ apiKey?: string;
5
+ fetchImpl?: typeof fetch;
6
+ }): (url: string) => Promise<WebFetchResponse>;
@@ -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 { createWebFetchHeadlessTool } from '../tools/web-fetch-headless.js';
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 = createWebSearchTool(), fetchPage = createWebFetchTool(), headlessFetch = createWebFetchHeadlessTool() } = {}) {
7
- const worker = createResearchWorker({ search, fetchPage });
8
- return createResearchOrchestrator({ worker, headlessFetch });
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(parsed)
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) {
@@ -1,6 +1,8 @@
1
1
  function internalReaderLabel(method) {
2
2
  if (method === 'headless')
3
3
  return 'web_fetch_headless';
4
+ if (method === 'firecrawl')
5
+ return 'firecrawl';
4
6
  if (method === 'http')
5
7
  return 'web_fetch';
6
8
  return 'web_explore';
@@ -0,0 +1,7 @@
1
+ import type { WebSearchResponse } from '../types.js';
2
+ export declare function createSearxngSearchTool({ baseUrl, fetchImpl }: {
3
+ baseUrl: string;
4
+ fetchImpl?: typeof fetch;
5
+ }): ({ query }: {
6
+ query: string;
7
+ }) => Promise<WebSearchResponse>;
@@ -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
+ }
@@ -31,7 +31,7 @@ export declare function createWebExploreTool({ explore }?: {
31
31
  sources: Array<{
32
32
  title: string;
33
33
  url: string;
34
- method?: "http" | "headless";
34
+ method?: "http" | "headless" | "firecrawl";
35
35
  }>;
36
36
  caveat?: string;
37
37
  metadata?: {
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.4.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.67.2",
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
  }