@rol1n/web-reach 0.1.0 → 0.1.1

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
@@ -1,6 +1,6 @@
1
1
  # web-reach
2
2
 
3
- Web search and page extraction tools for Pi. Registers `web_search` through Exa's public MCP endpoint and `web_fetch` for readable Markdown or raw responses.
3
+ Keyless web search (`web_search` via Exa MCP) and multi-tier page extraction (`web_fetch` supporting Next.js RSC, semantic HTML-to-Markdown, optional `unpdf` PDF extraction, and Jina Reader fallback).
4
4
 
5
5
  ## Install
6
6
 
@@ -8,30 +8,12 @@ Web search and page extraction tools for Pi. Registers `web_search` through Exa'
8
8
  pi install npm:@rol1n/web-reach
9
9
  ```
10
10
 
11
- Also included in `@rol1n/pi-kit`. Avoid enabling another extension that registers the same tool names unless overriding them is intentional.
12
-
13
- ## Use
14
-
15
- ```json
16
- {"query":"Pi coding agent package resource filtering"}
17
- ```
18
-
19
- ```json
20
- {"url":"https://example.com/docs","mode":"readable"}
21
- ```
22
-
23
- The first object is for `web_search`; the second is for `web_fetch`. Fetch mode defaults to `readable` and also accepts `raw`. Extraction includes HTML-to-Markdown, JSON formatting, PDF handling, and a Jina Reader fallback for pages that need it. Returned text is limited to 35,000 characters.
11
+ Also included in `@rol1n/pi-kit`.
24
12
 
25
13
  ## Configuration
26
14
 
27
15
  | Environment variable | Default |
28
16
  |---|---|
29
- | `SRP_WEB_SEARCH_TIMEOUT_MS` | 60000 |
30
- | `SRP_WEB_FETCH_TIMEOUT_MS` | 20000 |
31
- | `SRP_WEB_FETCH_MAX_BYTES` | 4194304 (4 MiB) |
32
-
33
- No separate search API key is configured; availability and rate limits depend on the public Exa and Jina services. PDF extraction optionally imports `unpdf` if it is available. Without that package the implementation falls back to a basic text extraction path, with lower PDF text quality.
34
-
35
- ## Source layout
36
-
37
- `index.ts` registers the tools and re-exports `searchWeb` and `fetchUrl`. `search.ts` handles Exa MCP requests and response parsing; `transport.ts` shares timeout, cancellation, environment, and truncation helpers. `page.ts` coordinates page fetching and extraction, using `rsc.ts` for Next.js data, `html.ts` for semantic Markdown, `pdf.ts` for optional PDF extraction, and `reader.ts` for the Jina Reader fallback.
17
+ | `SRP_WEB_SEARCH_TIMEOUT_MS` | `60000` |
18
+ | `SRP_WEB_FETCH_TIMEOUT_MS` | `20000` |
19
+ | `SRP_WEB_FETCH_MAX_BYTES` | `4194304` (4 MiB) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rol1n/web-reach",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "description": "web-reach extension from pi-kit",
6
6
  "keywords": [
@@ -9,7 +9,7 @@
9
9
  "repository": {
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/RolinShmily/pi-kit.git",
12
- "directory": "extensions/web-reach"
12
+ "directory": "web-reach"
13
13
  },
14
14
  "homepage": "https://github.com/RolinShmily/pi-kit#readme",
15
15
  "bugs": {
@@ -21,10 +21,6 @@
21
21
  "publishConfig": {
22
22
  "access": "public"
23
23
  },
24
- "files": [
25
- "extensions/web-reach",
26
- "extensions/web-reach/README.md"
27
- ],
28
24
  "peerDependencies": {
29
25
  "@earendil-works/pi-ai": "*",
30
26
  "@earendil-works/pi-agent-core": "*",
@@ -34,7 +30,7 @@
34
30
  },
35
31
  "pi": {
36
32
  "extensions": [
37
- "./extensions/web-reach/index.ts"
33
+ "./index.ts"
38
34
  ],
39
35
  "skills": [],
40
36
  "prompts": [],
@@ -1,8 +1,90 @@
1
1
  import { DEFAULT_USER_AGENT, envNum, truncate, withTimeout } from "./transport.ts";
2
2
  import { extractRSCContent } from "./rsc.ts";
3
3
  import { extractTitleFromHtml, htmlToMarkdown, isLikelyJSRendered } from "./html.ts";
4
- import { fetchViaJinaReader } from "./reader.ts";
5
- import { extractPdfBuffer, isPdfUrl } from "./pdf.ts";
4
+
5
+ const JINA_READER_BASE = "https://r.jina.ai/";
6
+
7
+ export async function fetchViaJinaReader(
8
+ url: string,
9
+ signal?: AbortSignal,
10
+ ): Promise<{ text: string; title: string } | null> {
11
+ const { ac, done } = withTimeout(30_000, "Jina Reader timed out", signal);
12
+ try {
13
+ const res = await fetch(`${JINA_READER_BASE}${url}`, {
14
+ headers: {
15
+ Accept: "text/markdown",
16
+ "X-No-Cache": "true",
17
+ "User-Agent": DEFAULT_USER_AGENT,
18
+ },
19
+ signal: ac.signal,
20
+ });
21
+ if (!res.ok) return null;
22
+
23
+ const raw = await res.text();
24
+ const contentIndex = raw.indexOf("Markdown Content:");
25
+ const markdown = contentIndex >= 0 ? raw.slice(contentIndex + 17).trim() : raw.trim();
26
+
27
+ if (
28
+ markdown.length < 80 ||
29
+ markdown.startsWith("Loading...") ||
30
+ markdown.startsWith("Please enable JavaScript")
31
+ ) {
32
+ return null;
33
+ }
34
+
35
+ const titleMatch = markdown.match(/^#+\s+(.+)$/m);
36
+ const title = titleMatch?.[1]?.trim() || "";
37
+
38
+ return { text: markdown, title };
39
+ } catch {
40
+ return null;
41
+ } finally {
42
+ done();
43
+ }
44
+ }
45
+
46
+ export function isPdfUrl(url: string, contentType?: string): boolean {
47
+ if (contentType?.includes("application/pdf")) return true;
48
+ try {
49
+ return new URL(url).pathname.toLowerCase().endsWith(".pdf");
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ export async function extractPdfBuffer(
56
+ buffer: ArrayBuffer,
57
+ url: string,
58
+ ): Promise<{ text: string; title: string }> {
59
+ try {
60
+ const { getDocumentProxy } = await import("unpdf");
61
+ const pdf = await getDocumentProxy(new Uint8Array(buffer));
62
+ const maxPages = Math.min(pdf.numPages, 80);
63
+ const pages: string[] = [];
64
+
65
+ for (let i = 1; i <= maxPages; i++) {
66
+ const page = await pdf.getPage(i);
67
+ const textContent = await page.getTextContent();
68
+ const pageText = textContent.items
69
+ .map((item: unknown) => (item as { str?: string }).str || "")
70
+ .join(" ")
71
+ .replace(/\s+/g, " ")
72
+ .trim();
73
+ if (pageText) pages.push(`--- Page ${i} ---\n${pageText}`);
74
+ }
75
+
76
+ const title = new URL(url).pathname.split("/").pop() || "PDF Document";
77
+ const text = `# ${title}\n\nPages: ${pdf.numPages}\n\n${pages.join("\n\n")}`;
78
+ return { text, title };
79
+ } catch {
80
+ // Fallback plain text extraction
81
+ const text = Buffer.from(buffer).toString("latin1").replace(/[^\x20-\x7E\n\r\t]/g, " ");
82
+ return {
83
+ text: `# PDF Document\n\n${text.slice(0, 8000)}`,
84
+ title: "PDF Document",
85
+ };
86
+ }
87
+ }
6
88
 
7
89
  const MIN_USEFUL_CONTENT_LENGTH = 300;
8
90
 
@@ -1,37 +0,0 @@
1
- # web-reach
2
-
3
- Web search and page extraction tools for Pi. Registers `web_search` through Exa's public MCP endpoint and `web_fetch` for readable Markdown or raw responses.
4
-
5
- ## Install
6
-
7
- ```sh
8
- pi install npm:@rol1n/web-reach
9
- ```
10
-
11
- Also included in `@rol1n/pi-kit`. Avoid enabling another extension that registers the same tool names unless overriding them is intentional.
12
-
13
- ## Use
14
-
15
- ```json
16
- {"query":"Pi coding agent package resource filtering"}
17
- ```
18
-
19
- ```json
20
- {"url":"https://example.com/docs","mode":"readable"}
21
- ```
22
-
23
- The first object is for `web_search`; the second is for `web_fetch`. Fetch mode defaults to `readable` and also accepts `raw`. Extraction includes HTML-to-Markdown, JSON formatting, PDF handling, and a Jina Reader fallback for pages that need it. Returned text is limited to 35,000 characters.
24
-
25
- ## Configuration
26
-
27
- | Environment variable | Default |
28
- |---|---|
29
- | `SRP_WEB_SEARCH_TIMEOUT_MS` | 60000 |
30
- | `SRP_WEB_FETCH_TIMEOUT_MS` | 20000 |
31
- | `SRP_WEB_FETCH_MAX_BYTES` | 4194304 (4 MiB) |
32
-
33
- No separate search API key is configured; availability and rate limits depend on the public Exa and Jina services. PDF extraction optionally imports `unpdf` if it is available. Without that package the implementation falls back to a basic text extraction path, with lower PDF text quality.
34
-
35
- ## Source layout
36
-
37
- `index.ts` registers the tools and re-exports `searchWeb` and `fetchUrl`. `search.ts` handles Exa MCP requests and response parsing; `transport.ts` shares timeout, cancellation, environment, and truncation helpers. `page.ts` coordinates page fetching and extraction, using `rsc.ts` for Next.js data, `html.ts` for semantic Markdown, `pdf.ts` for optional PDF extraction, and `reader.ts` for the Jina Reader fallback.
@@ -1,42 +0,0 @@
1
- export function isPdfUrl(url: string, contentType?: string): boolean {
2
- if (contentType?.includes("application/pdf")) return true;
3
- try {
4
- return new URL(url).pathname.toLowerCase().endsWith(".pdf");
5
- } catch {
6
- return false;
7
- }
8
- }
9
-
10
- export async function extractPdfBuffer(
11
- buffer: ArrayBuffer,
12
- url: string,
13
- ): Promise<{ text: string; title: string }> {
14
- try {
15
- const { getDocumentProxy } = await import("unpdf");
16
- const pdf = await getDocumentProxy(new Uint8Array(buffer));
17
- const maxPages = Math.min(pdf.numPages, 80);
18
- const pages: string[] = [];
19
-
20
- for (let i = 1; i <= maxPages; i++) {
21
- const page = await pdf.getPage(i);
22
- const textContent = await page.getTextContent();
23
- const pageText = textContent.items
24
- .map((item: unknown) => (item as { str?: string }).str || "")
25
- .join(" ")
26
- .replace(/\s+/g, " ")
27
- .trim();
28
- if (pageText) pages.push(`--- Page ${i} ---\n${pageText}`);
29
- }
30
-
31
- const title = new URL(url).pathname.split("/").pop() || "PDF Document";
32
- const text = `# ${title}\n\nPages: ${pdf.numPages}\n\n${pages.join("\n\n")}`;
33
- return { text, title };
34
- } catch {
35
- // Fallback plain text extraction
36
- const text = Buffer.from(buffer).toString("latin1").replace(/[^\x20-\x7E\n\r\t]/g, " ");
37
- return {
38
- text: `# PDF Document\n\n${text.slice(0, 8000)}`,
39
- title: "PDF Document",
40
- };
41
- }
42
- }
@@ -1,42 +0,0 @@
1
- import { DEFAULT_USER_AGENT, withTimeout } from "./transport.ts";
2
-
3
- const JINA_READER_BASE = "https://r.jina.ai/";
4
-
5
- export async function fetchViaJinaReader(
6
- url: string,
7
- signal?: AbortSignal,
8
- ): Promise<{ text: string; title: string } | null> {
9
- const { ac, done } = withTimeout(30_000, "Jina Reader timed out", signal);
10
- try {
11
- const res = await fetch(`${JINA_READER_BASE}${url}`, {
12
- headers: {
13
- Accept: "text/markdown",
14
- "X-No-Cache": "true",
15
- "User-Agent": DEFAULT_USER_AGENT,
16
- },
17
- signal: ac.signal,
18
- });
19
- if (!res.ok) return null;
20
-
21
- const raw = await res.text();
22
- const contentIndex = raw.indexOf("Markdown Content:");
23
- const markdown = contentIndex >= 0 ? raw.slice(contentIndex + 17).trim() : raw.trim();
24
-
25
- if (
26
- markdown.length < 80 ||
27
- markdown.startsWith("Loading...") ||
28
- markdown.startsWith("Please enable JavaScript")
29
- ) {
30
- return null;
31
- }
32
-
33
- const titleMatch = markdown.match(/^#+\s+(.+)$/m);
34
- const title = titleMatch?.[1]?.trim() || "";
35
-
36
- return { text: markdown, title };
37
- } catch {
38
- return null;
39
- } finally {
40
- done();
41
- }
42
- }
File without changes
File without changes
File without changes
File without changes