@demigodmode/pi-web-agent 1.7.2 → 1.9.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/backends/config.d.ts +8 -0
  3. package/dist/backends/config.js +45 -0
  4. package/dist/backends/doctor.js +33 -0
  5. package/dist/backends/factory.js +45 -2
  6. package/dist/commands/web-agent-config.js +63 -6
  7. package/dist/extension.js +53 -12
  8. package/dist/orchestration/direct-url.js +2 -25
  9. package/dist/orchestration/evidence-quality.d.ts +1 -0
  10. package/dist/orchestration/evidence-quality.js +4 -2
  11. package/dist/orchestration/evidence-ranker.js +2 -0
  12. package/dist/orchestration/index.d.ts +2 -0
  13. package/dist/orchestration/research-orchestrator.d.ts +3 -1
  14. package/dist/orchestration/research-orchestrator.js +63 -6
  15. package/dist/orchestration/research-types.d.ts +7 -2
  16. package/dist/orchestration/research-worker.js +26 -3
  17. package/dist/orchestration/url.d.ts +6 -0
  18. package/dist/orchestration/url.js +34 -0
  19. package/dist/presentation/explore-presentation.js +18 -4
  20. package/dist/presentation/search-presentation.js +14 -2
  21. package/dist/readers/github-reader.d.ts +7 -0
  22. package/dist/readers/github-reader.js +130 -0
  23. package/dist/readers/limits.d.ts +3 -0
  24. package/dist/readers/limits.js +3 -0
  25. package/dist/readers/pdf-reader.d.ts +11 -0
  26. package/dist/readers/pdf-reader.js +76 -0
  27. package/dist/readers/resolver.d.ts +12 -0
  28. package/dist/readers/resolver.js +16 -0
  29. package/dist/readers/types.d.ts +10 -0
  30. package/dist/readers/types.js +1 -0
  31. package/dist/readers/youtube-reader.d.ts +21 -0
  32. package/dist/readers/youtube-reader.js +71 -0
  33. package/dist/search/fanout.d.ts +14 -0
  34. package/dist/search/fanout.js +118 -0
  35. package/dist/tools/web-explore.d.ts +3 -1
  36. package/dist/types.d.ts +13 -2
  37. package/package.json +4 -2
@@ -0,0 +1,71 @@
1
+ import { getSubtitles, getVideoDetails } from 'youtube-caption-extractor';
2
+ import { READER_TEXT_CAP } from './limits.js';
3
+ function extractVideoId(url) {
4
+ let parsed;
5
+ try {
6
+ parsed = new URL(url);
7
+ }
8
+ catch {
9
+ return undefined;
10
+ }
11
+ const host = parsed.hostname.toLowerCase().replace(/^www\./, '');
12
+ if (host === 'youtu.be') {
13
+ return parsed.pathname.split('/').filter(Boolean)[0];
14
+ }
15
+ if (host === 'youtube.com') {
16
+ if (parsed.pathname === '/watch')
17
+ return parsed.searchParams.get('v') ?? undefined;
18
+ const [prefix, id] = parsed.pathname.split('/').filter(Boolean);
19
+ if ((prefix === 'shorts' || prefix === 'live' || prefix === 'embed') && id)
20
+ return id;
21
+ }
22
+ return undefined;
23
+ }
24
+ export function createYoutubeReader({ fetchSubtitles = getSubtitles, fetchDetails = getVideoDetails } = {}) {
25
+ return {
26
+ name: 'youtube',
27
+ canHandle(url) {
28
+ return extractVideoId(url) !== undefined;
29
+ },
30
+ async read(url) {
31
+ const videoID = extractVideoId(url);
32
+ if (!videoID) {
33
+ return {
34
+ status: 'error',
35
+ url,
36
+ metadata: { method: 'youtube', cacheHit: false },
37
+ error: { code: 'YOUTUBE_READ_FAILED', message: 'Could not extract a video id.' }
38
+ };
39
+ }
40
+ try {
41
+ const [subtitles, details] = await Promise.all([
42
+ fetchSubtitles({ videoID, lang: 'en' }),
43
+ fetchDetails({ videoID, lang: 'en' }).catch(() => ({ title: undefined }))
44
+ ]);
45
+ if (!subtitles || subtitles.length === 0) {
46
+ return {
47
+ status: 'unsupported',
48
+ url,
49
+ metadata: { method: 'youtube', cacheHit: false },
50
+ error: { code: 'YOUTUBE_NO_CAPTIONS', message: 'No captions available for this video.' }
51
+ };
52
+ }
53
+ const transcript = subtitles.map((line) => line.text).join(' ').replace(/\s+/g, ' ').trim();
54
+ return {
55
+ status: 'ok',
56
+ url,
57
+ content: { title: details.title ?? `YouTube ${videoID}`, text: transcript.slice(0, READER_TEXT_CAP) },
58
+ metadata: { method: 'youtube', cacheHit: false, truncated: transcript.length >= READER_TEXT_CAP }
59
+ };
60
+ }
61
+ catch (err) {
62
+ return {
63
+ status: 'error',
64
+ url,
65
+ metadata: { method: 'youtube', cacheHit: false },
66
+ error: { code: 'YOUTUBE_READ_FAILED', message: err instanceof Error ? err.message : 'YouTube read failed.' }
67
+ };
68
+ }
69
+ }
70
+ };
71
+ }
@@ -0,0 +1,14 @@
1
+ import type { FanoutMode, SearchProviderName, WebSearchResponse } from '../types.js';
2
+ export type FanoutProvider = {
3
+ name: SearchProviderName;
4
+ search: (input: {
5
+ query: string;
6
+ }) => Promise<WebSearchResponse>;
7
+ };
8
+ export declare function createFanoutSearch({ providers, mode, timeoutMs }: {
9
+ providers: FanoutProvider[];
10
+ mode: Exclude<FanoutMode, 'off'>;
11
+ timeoutMs?: number;
12
+ }): ({ query }: {
13
+ query: string;
14
+ }) => Promise<WebSearchResponse>;
@@ -0,0 +1,118 @@
1
+ import { buildSearchPresentation } from '../presentation/search-presentation.js';
2
+ import { canonicalizeUrl } from '../orchestration/url.js';
3
+ const FANOUT_MIN_RESULTS = 3;
4
+ function hostOf(url) {
5
+ try {
6
+ return new URL(url).hostname.toLowerCase().replace(/^www\./, '');
7
+ }
8
+ catch {
9
+ return undefined;
10
+ }
11
+ }
12
+ function primaryLooksWeak(results) {
13
+ if (results.length < FANOUT_MIN_RESULTS)
14
+ return true;
15
+ const hosts = new Set(results.map((r) => hostOf(r.url)).filter(Boolean));
16
+ return hosts.size <= 1;
17
+ }
18
+ function merge(lists) {
19
+ const byKey = new Map();
20
+ for (const { name, results } of lists) {
21
+ const seenThisProvider = new Set();
22
+ results.forEach((result, index) => {
23
+ const key = canonicalizeUrl(result.url) ?? result.url;
24
+ if (seenThisProvider.has(key))
25
+ return; // ignore duplicates within a single provider's own list
26
+ seenThisProvider.add(key);
27
+ const existing = byKey.get(key);
28
+ if (!existing) {
29
+ byKey.set(key, { result: { ...result }, providers: new Set([name]), bestRank: index });
30
+ return;
31
+ }
32
+ existing.providers.add(name);
33
+ existing.bestRank = Math.min(existing.bestRank, index);
34
+ if ((result.title?.length ?? 0) > (existing.result.title?.length ?? 0))
35
+ existing.result.title = result.title;
36
+ if ((result.snippet?.length ?? 0) > (existing.result.snippet?.length ?? 0))
37
+ existing.result.snippet = result.snippet;
38
+ });
39
+ }
40
+ return [...byKey.values()]
41
+ .sort((a, b) => b.providers.size - a.providers.size || a.bestRank - b.bestRank)
42
+ .map((entry) => entry.result);
43
+ }
44
+ function withPresentation(result) {
45
+ return { ...result, presentation: buildSearchPresentation(result) };
46
+ }
47
+ const FANOUT_PROVIDER_TIMEOUT_MS = 8000;
48
+ /** Resolve to undefined if the provider doesn't answer in time, so one slow/unreachable
49
+ * provider (e.g. a down self-hosted SearXNG) can't stall the whole fanout across passes. */
50
+ function withTimeout(promise, ms) {
51
+ return new Promise((resolve) => {
52
+ const timer = setTimeout(() => resolve(undefined), ms);
53
+ promise.then((value) => {
54
+ clearTimeout(timer);
55
+ resolve(value);
56
+ }, () => {
57
+ clearTimeout(timer);
58
+ resolve(undefined);
59
+ });
60
+ });
61
+ }
62
+ export function createFanoutSearch({ providers, mode, timeoutMs = FANOUT_PROVIDER_TIMEOUT_MS }) {
63
+ return async function fanoutSearch({ query }) {
64
+ const [primary, ...rest] = providers;
65
+ async function runSet(set) {
66
+ const settled = await Promise.all(set.map((p) => withTimeout(p.search({ query }), timeoutMs)));
67
+ const contributing = [];
68
+ const skipped = [];
69
+ set.forEach((provider, i) => {
70
+ const value = settled[i];
71
+ if (value && value.status === 'ok' && value.results.length > 0) {
72
+ contributing.push({ name: provider.name, results: value.results });
73
+ }
74
+ else {
75
+ skipped.push(provider.name);
76
+ }
77
+ });
78
+ return { contributing, skipped };
79
+ }
80
+ function finalize(lists, skipped, resolvedMode) {
81
+ if (lists.length === 0) {
82
+ const fanout = { mode: resolvedMode, providers: [] };
83
+ if (skipped.length > 0)
84
+ fanout.skipped = skipped;
85
+ return withPresentation({
86
+ status: 'error',
87
+ results: [],
88
+ metadata: { backend: primary.name, cacheHit: false, fanout },
89
+ error: { code: 'FANOUT_NO_RESULTS', message: 'No fanout provider returned usable results.' }
90
+ });
91
+ }
92
+ const fanout = { mode: resolvedMode, providers: lists.map((l) => l.name) };
93
+ if (skipped.length > 0)
94
+ fanout.skipped = skipped;
95
+ return withPresentation({
96
+ status: 'ok',
97
+ results: merge(lists),
98
+ metadata: { backend: primary.name, cacheHit: false, fanout }
99
+ });
100
+ }
101
+ if (mode === 'auto') {
102
+ const primaryOutcome = await withTimeout(primary.search({ query }), timeoutMs);
103
+ const primaryResults = primaryOutcome?.status === 'ok' ? primaryOutcome.results : [];
104
+ if (primaryResults.length > 0 && !primaryLooksWeak(primaryResults)) {
105
+ return withPresentation(primaryOutcome); // strong primary: no fanout
106
+ }
107
+ const { contributing, skipped } = await runSet(rest);
108
+ const lists = [
109
+ ...(primaryResults.length ? [{ name: primary.name, results: primaryResults }] : []),
110
+ ...contributing
111
+ ];
112
+ const allSkipped = [...(primaryResults.length ? [] : [primary.name]), ...skipped];
113
+ return finalize(lists, allSkipped, 'auto');
114
+ }
115
+ const { contributing, skipped } = await runSet(providers);
116
+ return finalize(contributing, skipped, 'on');
117
+ };
118
+ }
@@ -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" | "firecrawl";
34
+ method?: import("../types.js").FetchMethod;
35
35
  }>;
36
36
  caveat?: string;
37
37
  metadata?: {
@@ -40,6 +40,8 @@ export declare function createWebExploreTool({ explore }?: {
40
40
  headlessAttempts: number;
41
41
  exhaustedBudget: boolean;
42
42
  caveatReasons?: string[];
43
+ fanoutProviders?: import("../types.js").SearchProviderName[];
44
+ fanoutSkipped?: import("../types.js").SearchProviderName[];
43
45
  };
44
46
  error?: import("../types.js").ToolError;
45
47
  }>;
package/dist/types.d.ts CHANGED
@@ -6,6 +6,13 @@ export type SearchResult = {
6
6
  url: string;
7
7
  snippet: string;
8
8
  };
9
+ export type SearchProviderName = 'duckduckgo' | 'searxng' | 'brave' | 'youcom' | 'exa' | 'tavily';
10
+ export type FanoutMode = 'off' | 'on' | 'auto';
11
+ export type FanoutMetadata = {
12
+ mode: Exclude<FanoutMode, 'off'>;
13
+ providers: SearchProviderName[];
14
+ skipped?: SearchProviderName[];
15
+ };
9
16
  export type ToolError = {
10
17
  code: string;
11
18
  message: string;
@@ -15,9 +22,11 @@ export type SearchMetadata = {
15
22
  cacheHit: boolean;
16
23
  fallbackFrom?: 'searxng' | 'brave' | 'youcom' | 'exa' | 'tavily';
17
24
  fallbackReason?: string;
25
+ fanout?: FanoutMetadata;
18
26
  };
27
+ export type FetchMethod = 'http' | 'headless' | 'firecrawl' | 'github' | 'pdf' | 'youtube';
19
28
  export type FetchMetadata = {
20
- method: 'http' | 'headless' | 'firecrawl';
29
+ method: FetchMethod;
21
30
  cacheHit: boolean;
22
31
  fallbackFrom?: 'firecrawl';
23
32
  fallbackReason?: string;
@@ -60,7 +69,7 @@ export type WebExploreResponse = {
60
69
  sources: Array<{
61
70
  title: string;
62
71
  url: string;
63
- method?: 'http' | 'headless' | 'firecrawl';
72
+ method?: FetchMethod;
64
73
  }>;
65
74
  caveat?: string;
66
75
  metadata?: {
@@ -69,6 +78,8 @@ export type WebExploreResponse = {
69
78
  headlessAttempts: number;
70
79
  exhaustedBudget: boolean;
71
80
  caveatReasons?: string[];
81
+ fanoutProviders?: SearchProviderName[];
82
+ fanoutSkipped?: SearchProviderName[];
72
83
  };
73
84
  presentation?: PresentationEnvelope;
74
85
  error?: ToolError;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@demigodmode/pi-web-agent",
3
- "version": "1.7.2",
3
+ "version": "1.9.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",
@@ -67,7 +67,9 @@
67
67
  "cheerio": "^1.1.0",
68
68
  "jsdom": "^26.0.0",
69
69
  "playwright": "^1.60.0",
70
- "typebox": "^1.1.37"
70
+ "typebox": "^1.1.37",
71
+ "unpdf": "^1.8.1",
72
+ "youtube-caption-extractor": "^1.10.2"
71
73
  },
72
74
  "devDependencies": {
73
75
  "@earendil-works/pi-coding-agent": "^0.80.10",