@demigodmode/pi-web-agent 1.0.0 → 1.1.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 CHANGED
@@ -18,6 +18,21 @@ The format is intentionally simple and release-oriented.
18
18
  ### Breaking
19
19
  - None.
20
20
 
21
+ ## [1.1.0] - 2026-05-25
22
+ ### Added
23
+ - Added explicit opt-in fallback from SearXNG to DuckDuckGo and Firecrawl to HTTP.
24
+ - Added supported SearXNG and Firecrawl backend options in config.
25
+ - Added env-gated live tests for local SearXNG and Firecrawl instances.
26
+
27
+ ### Changed
28
+ - `/web-agent show` and `/web-agent doctor` now report configured backend fallback/options.
29
+
30
+ ### Fixed
31
+ - Nothing yet.
32
+
33
+ ### Breaking
34
+ - None.
35
+
21
36
  ## [1.0.0] - 2026-05-08
22
37
  ### Added
23
38
  - Added one-time `pi-web-agent` changelog notices after package updates and `/web-agent changelog` for manual viewing.
@@ -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;
@@ -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) {
@@ -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({ url: 'https://example.com', formats: ['markdown'] }),
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 declare function createBackendSet(config?: BackendConfig): BackendSet;
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;
@@ -34,25 +34,72 @@ function invalidFirecrawlFetch() {
34
34
  return { ...result, presentation: buildFetchPresentation(result) };
35
35
  };
36
36
  }
37
- export function createBackendSet(config = DEFAULT_BACKEND_CONFIG) {
38
- const search = config.search.provider === 'searxng'
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
- ? createSearxngSearchTool({ baseUrl: config.search.baseUrl })
79
+ ? createSearxngSearch({ baseUrl: config.search.baseUrl, options: config.search.options })
41
80
  : invalidSearxngSearch()
42
- : createWebSearchTool();
43
- const fetchPage = config.fetch.provider === 'firecrawl'
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
- ? createWebFetchTool({
46
- fetchPage: createFirecrawlFetcher({
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
- : createWebFetchTool({ fetchPage: invalidFirecrawlFetch() })
52
- : createWebFetchTool();
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: createWebFetchHeadlessTool()
103
+ headlessFetch: createHeadlessFetch()
57
104
  };
58
105
  }
@@ -25,16 +25,33 @@ async function defaultCheckTypebox() {
25
25
  return false;
26
26
  }
27
27
  }
28
+ function formatSearchOptions(config) {
29
+ return [
30
+ config.fallback ? `fallback ${config.fallback}` : undefined,
31
+ config.options?.categories?.length ? `categories ${config.options.categories.join(',')}` : undefined,
32
+ config.options?.language ? `language ${config.options.language}` : undefined,
33
+ config.options?.safesearch !== undefined ? `safesearch ${config.options.safesearch}` : undefined
34
+ ].filter(Boolean).join(' ');
35
+ }
36
+ function formatFetchOptions(config) {
37
+ return [
38
+ config.fallback ? `fallback ${config.fallback}` : undefined,
39
+ config.options?.formats?.length ? `formats ${config.options.formats.join(',')}` : undefined,
40
+ config.options?.onlyMainContent !== undefined ? `onlyMainContent ${config.options.onlyMainContent}` : undefined
41
+ ].filter(Boolean).join(' ');
42
+ }
28
43
  function formatBackendSummary(config = DEFAULT_BACKEND_CONFIG) {
29
- const search = config.search.baseUrl
44
+ const searchSuffix = formatSearchOptions(config.search);
45
+ const fetchSuffix = formatFetchOptions(config.fetch);
46
+ const searchBase = config.search.baseUrl
30
47
  ? `search: ${config.search.provider} (${config.search.baseUrl})`
31
48
  : `search: ${config.search.provider}`;
32
- const fetch = config.fetch.baseUrl
49
+ const fetchBase = config.fetch.baseUrl
33
50
  ? `fetch: ${config.fetch.provider} (${config.fetch.baseUrl})`
34
51
  : `fetch: ${config.fetch.provider}`;
35
52
  return [
36
- search,
37
- fetch,
53
+ searchSuffix ? `${searchBase} ${searchSuffix}` : searchBase,
54
+ fetchSuffix ? `${fetchBase} ${fetchSuffix}` : fetchBase,
38
55
  `headless: ${config.headless.provider}`
39
56
  ].join('\n');
40
57
  }
@@ -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({ url, formats: ['markdown'] })
22
+ body: JSON.stringify(body)
18
23
  });
19
24
  if (!response.ok) {
20
25
  throw new Error(`HTTP ${response.status}`);
@@ -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
- ? `Fetched page · article extracted${wordCount ? ` · ${wordCount} words` : ''}`
16
+ ? `${fallbackPrefix}Fetched page · article extracted${wordCount ? ` · ${wordCount} words` : ''}`
14
17
  : result.status === 'needs_headless'
15
- ? `Needs headless rendering: ${result.error?.message ?? 'Headless rendering recommended.'}`
16
- : `Fetch failed: ${result.error?.message ?? 'Unknown fetch failure.'}`;
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 `Search failed: ${result.error?.message ?? 'Unknown search failure.'}`;
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 `Found ${result.results.length} ${suffix}`;
9
+ return `${fallbackPrefix}Found ${result.results.length} ${suffix}`;
7
10
  }
8
11
  export function buildSearchPresentation(result) {
9
12
  const preview = result.results
@@ -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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@demigodmode/pi-web-agent",
3
- "version": "1.0.0",
3
+ "version": "1.1.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",