@demigodmode/pi-web-agent 1.7.1 → 1.8.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,35 @@ The format is intentionally simple and release-oriented.
18
18
  ### Breaking
19
19
  - None.
20
20
 
21
+ ## [1.8.0] - 2026-08-14
22
+ ### Added
23
+ - Read the content behind GitHub, PDF, and YouTube links directly instead of the page shell. GitHub files, issues and PRs come from the API/raw endpoints (optional `GITHUB_TOKEN` raises the rate limit), PDFs are parsed with unpdf, and YouTube links return the transcript. All keyless. Scanned PDFs and caption-less videos are caveated rather than failing. (#39, #40, #41)
24
+
25
+ ### Changed
26
+ - None.
27
+
28
+ ### Fixed
29
+ - None.
30
+
31
+ ### Breaking
32
+ - None.
33
+
34
+ ## [1.7.2] - 2026-08-12
35
+ ### Added
36
+ - None.
37
+
38
+ ### Changed
39
+ - None.
40
+
41
+ ### Fixed
42
+ - Candidate scoring now matches GitHub sources by parsed hostname instead of a `url.includes('github.com/')` substring check that could also match unrelated hosts like `evil.com/github.com/`.
43
+ - `decodeHtmlEntities` now decodes `&amp;` last so an already-encoded entity such as `&amp;lt;` no longer collapses into `<` (double-unescape).
44
+ - The CI workflow now sets an explicit read-only default `GITHUB_TOKEN` permission.
45
+ - Bumped transitive `postcss`, `undici`, and `brace-expansion` in the lockfile to clear reported advisories.
46
+
47
+ ### Breaking
48
+ - None.
49
+
21
50
  ## [1.7.1] - 2026-07-31
22
51
  ### Added
23
52
  - None.
@@ -10,6 +10,10 @@ import { createWebFetchHeadlessTool } from '../tools/web-fetch-headless.js';
10
10
  import { createWebFetchTool } from '../tools/web-fetch.js';
11
11
  import { createWebSearchTool } from '../tools/web-search.js';
12
12
  import { DEFAULT_BACKEND_CONFIG } from './config.js';
13
+ import { createSpecialContentResolver } from '../readers/resolver.js';
14
+ import { createGithubReader } from '../readers/github-reader.js';
15
+ import { createPdfReader } from '../readers/pdf-reader.js';
16
+ import { createYoutubeReader } from '../readers/youtube-reader.js';
13
17
  function invalidSearxngSearch() {
14
18
  return async function search() {
15
19
  const result = {
@@ -125,9 +129,13 @@ export function createBackendSet(config = DEFAULT_BACKEND_CONFIG, deps = {}) {
125
129
  if (config.fetch.provider === 'firecrawl' && config.fetch.fallback === 'http') {
126
130
  fetchPage = withFetchFallback(fetchPage, httpFetch);
127
131
  }
132
+ const fetchPageWithReaders = createSpecialContentResolver({
133
+ readers: [createGithubReader(), createPdfReader(), createYoutubeReader()],
134
+ fallback: fetchPage
135
+ });
128
136
  return {
129
137
  search,
130
- fetchPage,
138
+ fetchPage: fetchPageWithReaders,
131
139
  headlessFetch: createHeadlessFetch()
132
140
  };
133
141
  }
@@ -25,18 +25,24 @@ export function extractReadableContent(html, maxLength = 4000) {
25
25
  text
26
26
  };
27
27
  }
28
+ const NAMED_ENTITIES = {
29
+ '&nbsp;': ' ',
30
+ '&amp;': '&',
31
+ '&lt;': '<',
32
+ '&gt;': '>',
33
+ '&quot;': '"'
34
+ };
28
35
  function decodeHtmlEntities(text) {
29
- return text
30
- .replace(/&nbsp;/gi, ' ')
31
- .replace(/&amp;/gi, '&')
32
- .replace(/&lt;/gi, '<')
33
- .replace(/&gt;/gi, '>')
34
- .replace(/&quot;/gi, '"')
35
- .replace(/&#39;/gi, "'")
36
- .replace(/&#x27;/gi, "'")
37
- .replace(/&#x2F;/gi, '/')
38
- .replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
39
- .replace(/&#x([\da-f]+);/gi, (_, code) => String.fromCharCode(parseInt(code, 16)));
36
+ // Single pass so each entity is decoded exactly once. A multi-pass decode
37
+ // rescans its own output, which double-unescapes either &amp;lt; -> < or
38
+ // &#38;amp; -> & depending on the pass order. One pass avoids both.
39
+ return text.replace(/&(?:#(\d+)|#x([\da-f]+)|nbsp|amp|lt|gt|quot);/gi, (match, dec, hex) => {
40
+ if (dec !== undefined)
41
+ return String.fromCharCode(Number(dec));
42
+ if (hex !== undefined)
43
+ return String.fromCharCode(parseInt(hex, 16));
44
+ return NAMED_ENTITIES[match.toLowerCase()] ?? match;
45
+ });
40
46
  }
41
47
  function extractTitle(html) {
42
48
  const match = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
@@ -1,9 +1,10 @@
1
- import { classifySourceProfile } from './source-profile.js';
1
+ import { classifySourceProfile, hostOf } from './source-profile.js';
2
2
  function wantsDiscussionSources(query = '') {
3
3
  return /reddit|forum|forums|discussion|thread|comments|community|user experience|people recommend/i.test(query);
4
4
  }
5
5
  function candidateScore(result, query) {
6
- const url = result.url.toLowerCase();
6
+ const host = hostOf(result.url);
7
+ const isGithub = host === 'github.com' || (host?.endsWith('.github.com') ?? false);
7
8
  const profile = classifySourceProfile(result.url);
8
9
  const wantsThreads = wantsDiscussionSources(query);
9
10
  if (profile.kind === 'official-docs')
@@ -15,7 +16,7 @@ function candidateScore(result, query) {
15
16
  return 2;
16
17
  if (profile.kind === 'issue-thread')
17
18
  return 3;
18
- if (url.includes('github.com/'))
19
+ if (isGithub)
19
20
  return 4;
20
21
  if (profile.kind === 'package-page')
21
22
  return 6;
@@ -23,7 +24,7 @@ function candidateScore(result, query) {
23
24
  }
24
25
  if (profile.kind === 'issue-thread')
25
26
  return 2;
26
- if (url.includes('github.com/'))
27
+ if (isGithub)
27
28
  return 3;
28
29
  if (profile.kind === 'package-page')
29
30
  return 5;
@@ -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' | 'firecrawl';
2
+ export type ResearchMethod = 'search' | 'http' | 'headless' | 'firecrawl' | 'github' | 'pdf' | 'youtube';
3
3
  export type ResearchEvidence = {
4
4
  title: string;
5
5
  url: string;
@@ -5,4 +5,5 @@ export type SourceProfile = {
5
5
  sourceKind: ResearchSourceKind;
6
6
  shouldPreferHeadlessWhenWeak: boolean;
7
7
  };
8
+ export declare function hostOf(rawUrl: string): string | undefined;
8
9
  export declare function classifySourceProfile(rawUrl: string): SourceProfile;
@@ -10,6 +10,9 @@ function parseUrl(rawUrl) {
10
10
  return undefined;
11
11
  }
12
12
  }
13
+ export function hostOf(rawUrl) {
14
+ return parseUrl(rawUrl)?.hostname.toLowerCase().replace(/^www\./, '');
15
+ }
13
16
  function isOfficialApi(host, path) {
14
17
  return ((host === 'playwright.dev' && path.startsWith('/docs/api/')) ||
15
18
  (host === 'vitest.dev' && path.startsWith('/config/')));
@@ -5,6 +5,12 @@ function internalReaderLabel(method) {
5
5
  return 'firecrawl';
6
6
  if (method === 'http')
7
7
  return 'web_fetch';
8
+ if (method === 'github')
9
+ return 'github';
10
+ if (method === 'pdf')
11
+ return 'pdf';
12
+ if (method === 'youtube')
13
+ return 'youtube';
8
14
  return 'web_explore';
9
15
  }
10
16
  export function buildExplorePresentation(result) {
@@ -0,0 +1,7 @@
1
+ import type { SpecialContentReader } from './types.js';
2
+ type GithubReaderDeps = {
3
+ fetchImpl?: typeof fetch;
4
+ token?: string;
5
+ };
6
+ export declare function createGithubReader({ fetchImpl, token }?: GithubReaderDeps): SpecialContentReader;
7
+ export {};
@@ -0,0 +1,129 @@
1
+ function parseGithub(url) {
2
+ try {
3
+ return new URL(url);
4
+ }
5
+ catch {
6
+ return undefined;
7
+ }
8
+ }
9
+ function classifyGithubShape(url) {
10
+ const parsed = parseGithub(url);
11
+ if (!parsed || parsed.hostname.toLowerCase() !== 'github.com') {
12
+ return undefined;
13
+ }
14
+ const segments = parsed.pathname.split('/').filter(Boolean);
15
+ const [owner, repo, type, ...rest] = segments;
16
+ // blob: [owner, repo, 'blob', ref, ...path] with rest.length >= 2
17
+ if (owner && repo && type === 'blob' && rest.length >= 2) {
18
+ const [ref, ...pathParts] = rest;
19
+ return { shape: 'blob', owner, repo, ref, path: pathParts.join('/') };
20
+ }
21
+ // issue: [owner, repo, 'issues', N]
22
+ if (owner && repo && type === 'issues' && rest[0]) {
23
+ return { shape: 'issue', owner, repo, num: rest[0] };
24
+ }
25
+ // pull: [owner, repo, 'pull', N]
26
+ if (owner && repo && type === 'pull' && rest[0]) {
27
+ return { shape: 'pull', owner, repo, num: rest[0] };
28
+ }
29
+ // repo-root: exactly [owner, repo]
30
+ if (owner && repo && !type) {
31
+ return { shape: 'repo-root', owner, repo };
32
+ }
33
+ return undefined;
34
+ }
35
+ function encodeSegment(segment) {
36
+ let decoded;
37
+ try {
38
+ decoded = decodeURIComponent(segment);
39
+ }
40
+ catch {
41
+ decoded = segment;
42
+ }
43
+ return encodeURIComponent(decoded);
44
+ }
45
+ function fail(url, message) {
46
+ return {
47
+ status: 'error',
48
+ url,
49
+ metadata: { method: 'github', cacheHit: false },
50
+ error: { code: 'GITHUB_FETCH_FAILED', message }
51
+ };
52
+ }
53
+ function okResponse(url, title, text) {
54
+ const capped = text.slice(0, 4000);
55
+ return {
56
+ status: 'ok',
57
+ url,
58
+ content: { title, text: capped },
59
+ metadata: { method: 'github', cacheHit: false, truncated: text.length >= 4000 }
60
+ };
61
+ }
62
+ export function createGithubReader({ fetchImpl = fetch, token = process.env.GITHUB_TOKEN } = {}) {
63
+ function headers(json) {
64
+ const h = { 'User-Agent': 'pi-web-agent' };
65
+ if (json)
66
+ h.Accept = 'application/vnd.github+json';
67
+ if (token)
68
+ h.Authorization = `Bearer ${token}`;
69
+ return h;
70
+ }
71
+ async function getText(target) {
72
+ const res = await fetchImpl(target, { headers: headers(false) });
73
+ if (!res.ok)
74
+ throw new Error(`GitHub returned ${res.status} for ${target}`);
75
+ return res.text();
76
+ }
77
+ async function getJson(target) {
78
+ const res = await fetchImpl(target, { headers: headers(true) });
79
+ if (!res.ok)
80
+ throw new Error(`GitHub API returned ${res.status} for ${target}`);
81
+ return res.json();
82
+ }
83
+ async function readBlob(url, owner, repo, ref, path) {
84
+ const encodedPath = path.split('/').map(encodeSegment).join('/');
85
+ const raw = `https://raw.githubusercontent.com/${owner}/${repo}/${encodeSegment(ref)}/${encodedPath}`;
86
+ const text = await getText(raw);
87
+ return okResponse(url, `${owner}/${repo}/${path}`, text);
88
+ }
89
+ async function readThread(url, owner, repo, kind, num) {
90
+ const base = `https://api.github.com/repos/${owner}/${repo}/${kind}/${num}`;
91
+ const item = await getJson(base);
92
+ const comments = await getJson(`${base}/comments`);
93
+ const body = [item.body ?? '', ...comments.map((c) => c.body ?? '')].filter(Boolean).join('\n\n---\n\n');
94
+ return okResponse(url, item.title ?? `${owner}/${repo} ${kind} #${num}`, body);
95
+ }
96
+ async function readRepoRoot(url, owner, repo) {
97
+ const readmeMeta = await getJson(`https://api.github.com/repos/${owner}/${repo}/readme`);
98
+ const readme = readmeMeta.download_url ? await getText(readmeMeta.download_url) : '';
99
+ const tree = await getJson(`https://api.github.com/repos/${owner}/${repo}/contents`);
100
+ const listing = tree.map((entry) => `${entry.type === 'dir' ? '[dir] ' : ''}${entry.name}`).join('\n');
101
+ return okResponse(url, `${owner}/${repo}`, `${readme}\n\nTop-level contents:\n${listing}`);
102
+ }
103
+ return {
104
+ name: 'github',
105
+ canHandle(url) {
106
+ return classifyGithubShape(url) !== undefined;
107
+ },
108
+ async read(url) {
109
+ const shape = classifyGithubShape(url);
110
+ if (!shape)
111
+ return fail(url, 'Unsupported GitHub URL shape for the reader.');
112
+ try {
113
+ switch (shape.shape) {
114
+ case 'blob':
115
+ return await readBlob(url, shape.owner, shape.repo, shape.ref, shape.path);
116
+ case 'issue':
117
+ return await readThread(url, shape.owner, shape.repo, 'issues', shape.num);
118
+ case 'pull':
119
+ return await readThread(url, shape.owner, shape.repo, 'pulls', shape.num);
120
+ case 'repo-root':
121
+ return await readRepoRoot(url, shape.owner, shape.repo);
122
+ }
123
+ }
124
+ catch (err) {
125
+ return fail(url, err instanceof Error ? err.message : 'GitHub read failed.');
126
+ }
127
+ }
128
+ };
129
+ }
@@ -0,0 +1,11 @@
1
+ import type { SpecialContentReader } from './types.js';
2
+ type PdfReaderDeps = {
3
+ fetchImpl?: typeof fetch;
4
+ /** Injectable for tests; defaults to unpdf. */
5
+ extractPdfText?: (bytes: Uint8Array) => Promise<{
6
+ text: string;
7
+ title?: string;
8
+ }>;
9
+ };
10
+ export declare function createPdfReader({ fetchImpl, extractPdfText }?: PdfReaderDeps): SpecialContentReader;
11
+ export {};
@@ -0,0 +1,75 @@
1
+ import { extractText, getDocumentProxy, getMeta } from 'unpdf';
2
+ async function defaultExtract(bytes) {
3
+ const pdf = await getDocumentProxy(bytes);
4
+ const [{ text }, meta] = await Promise.all([
5
+ extractText(pdf, { mergePages: true }),
6
+ getMeta(pdf).catch(() => ({ info: undefined }))
7
+ ]);
8
+ const info = meta.info;
9
+ const rawTitle = info?.Title?.trim();
10
+ return { text, title: rawTitle ? rawTitle : undefined };
11
+ }
12
+ function isPdfUrl(url) {
13
+ try {
14
+ return new URL(url).pathname.toLowerCase().endsWith('.pdf');
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ function filenameFromUrl(url) {
21
+ try {
22
+ const last = new URL(url).pathname.split('/').filter(Boolean).pop() ?? 'PDF';
23
+ return decodeURIComponent(last);
24
+ }
25
+ catch {
26
+ return 'PDF';
27
+ }
28
+ }
29
+ export function createPdfReader({ fetchImpl = fetch, extractPdfText = defaultExtract } = {}) {
30
+ return {
31
+ name: 'pdf',
32
+ canHandle: isPdfUrl,
33
+ canHandleContentType(contentType) {
34
+ return contentType.toLowerCase().includes('application/pdf');
35
+ },
36
+ async read(url) {
37
+ try {
38
+ const response = await fetchImpl(url);
39
+ if (!('ok' in response) || !response.ok) {
40
+ return {
41
+ status: 'error',
42
+ url,
43
+ metadata: { method: 'pdf', cacheHit: false },
44
+ error: { code: 'PDF_READ_FAILED', message: `Fetching the PDF failed.` }
45
+ };
46
+ }
47
+ const bytes = new Uint8Array(await response.arrayBuffer());
48
+ const extracted = await extractPdfText(bytes);
49
+ const text = extracted.text.trim();
50
+ if (text.length === 0) {
51
+ return {
52
+ status: 'unsupported',
53
+ url,
54
+ metadata: { method: 'pdf', cacheHit: false, contentType: 'application/pdf' },
55
+ error: { code: 'PDF_NO_TEXT', message: 'This looks like a scanned PDF with no extractable text.' }
56
+ };
57
+ }
58
+ return {
59
+ status: 'ok',
60
+ url,
61
+ content: { title: extracted.title ?? filenameFromUrl(url), text: text.slice(0, 4000) },
62
+ metadata: { method: 'pdf', cacheHit: false, contentType: 'application/pdf', truncated: text.length >= 4000 }
63
+ };
64
+ }
65
+ catch (err) {
66
+ return {
67
+ status: 'error',
68
+ url,
69
+ metadata: { method: 'pdf', cacheHit: false },
70
+ error: { code: 'PDF_READ_FAILED', message: err instanceof Error ? err.message : 'PDF read failed.' }
71
+ };
72
+ }
73
+ }
74
+ };
75
+ }
@@ -0,0 +1,12 @@
1
+ import type { WebFetchResponse } from '../types.js';
2
+ import type { SpecialContentReader } from './types.js';
3
+ type ResolverDeps = {
4
+ readers: SpecialContentReader[];
5
+ fallback: (input: {
6
+ url: string;
7
+ }) => Promise<WebFetchResponse>;
8
+ };
9
+ export declare function createSpecialContentResolver({ readers, fallback }: ResolverDeps): (input: {
10
+ url: string;
11
+ }) => Promise<WebFetchResponse>;
12
+ export {};
@@ -0,0 +1,16 @@
1
+ export function createSpecialContentResolver({ readers, fallback }) {
2
+ return async function resolve(input) {
3
+ const matched = readers.find((reader) => reader.canHandle(input.url));
4
+ if (matched) {
5
+ return matched.read(input.url);
6
+ }
7
+ const response = await fallback(input);
8
+ if (response.status === 'unsupported' && response.metadata.contentType) {
9
+ const byContentType = readers.find((reader) => reader.canHandleContentType?.(response.metadata.contentType));
10
+ if (byContentType) {
11
+ return byContentType.read(input.url);
12
+ }
13
+ }
14
+ return response;
15
+ };
16
+ }
@@ -0,0 +1,10 @@
1
+ import type { WebFetchResponse } from '../types.js';
2
+ export type SpecialContentReader = {
3
+ name: string;
4
+ /** Cheap URL-shape check. No network. */
5
+ canHandle(url: string): boolean;
6
+ /** Optional: claim a response by content-type after a normal fetch (e.g. application/pdf). */
7
+ canHandleContentType?(contentType: string): boolean;
8
+ /** Produce a normal WebFetchResponse. Must never throw. */
9
+ read(url: string): Promise<WebFetchResponse>;
10
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import type { SpecialContentReader } from './types.js';
2
+ type Subtitle = {
3
+ start: string;
4
+ dur: string;
5
+ text: string;
6
+ };
7
+ type YoutubeReaderDeps = {
8
+ fetchSubtitles?: (input: {
9
+ videoID: string;
10
+ lang: string;
11
+ }) => Promise<Subtitle[]>;
12
+ fetchDetails?: (input: {
13
+ videoID: string;
14
+ lang: string;
15
+ }) => Promise<{
16
+ title?: string;
17
+ description?: string;
18
+ }>;
19
+ };
20
+ export declare function createYoutubeReader({ fetchSubtitles, fetchDetails }?: YoutubeReaderDeps): SpecialContentReader;
21
+ export {};
@@ -0,0 +1,70 @@
1
+ import { getSubtitles, getVideoDetails } from 'youtube-caption-extractor';
2
+ function extractVideoId(url) {
3
+ let parsed;
4
+ try {
5
+ parsed = new URL(url);
6
+ }
7
+ catch {
8
+ return undefined;
9
+ }
10
+ const host = parsed.hostname.toLowerCase().replace(/^www\./, '');
11
+ if (host === 'youtu.be') {
12
+ return parsed.pathname.split('/').filter(Boolean)[0];
13
+ }
14
+ if (host === 'youtube.com') {
15
+ if (parsed.pathname === '/watch')
16
+ return parsed.searchParams.get('v') ?? undefined;
17
+ const [prefix, id] = parsed.pathname.split('/').filter(Boolean);
18
+ if ((prefix === 'shorts' || prefix === 'live' || prefix === 'embed') && id)
19
+ return id;
20
+ }
21
+ return undefined;
22
+ }
23
+ export function createYoutubeReader({ fetchSubtitles = getSubtitles, fetchDetails = getVideoDetails } = {}) {
24
+ return {
25
+ name: 'youtube',
26
+ canHandle(url) {
27
+ return extractVideoId(url) !== undefined;
28
+ },
29
+ async read(url) {
30
+ const videoID = extractVideoId(url);
31
+ if (!videoID) {
32
+ return {
33
+ status: 'error',
34
+ url,
35
+ metadata: { method: 'youtube', cacheHit: false },
36
+ error: { code: 'YOUTUBE_READ_FAILED', message: 'Could not extract a video id.' }
37
+ };
38
+ }
39
+ try {
40
+ const [subtitles, details] = await Promise.all([
41
+ fetchSubtitles({ videoID, lang: 'en' }),
42
+ fetchDetails({ videoID, lang: 'en' }).catch(() => ({ title: undefined }))
43
+ ]);
44
+ if (!subtitles || subtitles.length === 0) {
45
+ return {
46
+ status: 'unsupported',
47
+ url,
48
+ metadata: { method: 'youtube', cacheHit: false },
49
+ error: { code: 'YOUTUBE_NO_CAPTIONS', message: 'No captions available for this video.' }
50
+ };
51
+ }
52
+ const transcript = subtitles.map((line) => line.text).join(' ').replace(/\s+/g, ' ').trim();
53
+ return {
54
+ status: 'ok',
55
+ url,
56
+ content: { title: details.title ?? `YouTube ${videoID}`, text: transcript.slice(0, 4000) },
57
+ metadata: { method: 'youtube', cacheHit: false, truncated: transcript.length >= 4000 }
58
+ };
59
+ }
60
+ catch (err) {
61
+ return {
62
+ status: 'error',
63
+ url,
64
+ metadata: { method: 'youtube', cacheHit: false },
65
+ error: { code: 'YOUTUBE_READ_FAILED', message: err instanceof Error ? err.message : 'YouTube read failed.' }
66
+ };
67
+ }
68
+ }
69
+ };
70
+ }
@@ -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?: {
package/dist/types.d.ts CHANGED
@@ -16,8 +16,9 @@ export type SearchMetadata = {
16
16
  fallbackFrom?: 'searxng' | 'brave' | 'youcom' | 'exa' | 'tavily';
17
17
  fallbackReason?: string;
18
18
  };
19
+ export type FetchMethod = 'http' | 'headless' | 'firecrawl' | 'github' | 'pdf' | 'youtube';
19
20
  export type FetchMetadata = {
20
- method: 'http' | 'headless' | 'firecrawl';
21
+ method: FetchMethod;
21
22
  cacheHit: boolean;
22
23
  fallbackFrom?: 'firecrawl';
23
24
  fallbackReason?: string;
@@ -60,7 +61,7 @@ export type WebExploreResponse = {
60
61
  sources: Array<{
61
62
  title: string;
62
63
  url: string;
63
- method?: 'http' | 'headless' | 'firecrawl';
64
+ method?: FetchMethod;
64
65
  }>;
65
66
  caveat?: string;
66
67
  metadata?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@demigodmode/pi-web-agent",
3
- "version": "1.7.1",
3
+ "version": "1.8.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",