pi-unsloth-webtools 0.7.2 → 0.7.3

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 (3) hide show
  1. package/README.md +10 -3
  2. package/index.ts +61 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -42,7 +42,8 @@ Mirrors Unsloth Studio's `web_search` tool:
42
42
  `Snippet:` blocks separated by `---`, ending with the hint to pass `{"url": "<URL>"}` to
43
43
  read a full page.
44
44
  - Accepts an optional `url` parameter; when given, fetches that page's text instead of
45
- searching (optionally truncated with `maxChars`).
45
+ searching (optionally truncated with `maxChars`). An HTTP 403 on that fetch falls back
46
+ to `web_render` when the tool is enabled.
46
47
  - Rate-limit, timeout, and empty-result messages mirror Studio's `_search_failure_message`.
47
48
  - Transient engine failures (network errors or null responses) are retried once with a short
48
49
  backoff inside the same timeout budget (a retry that cannot fit in the remaining budget is
@@ -119,6 +120,9 @@ Port of Studio's `_fetch_page_text` / `_fetch_url_raw` pipeline:
119
120
  `Date:` (`article:published_time` / `dc.date` / `date`) and `Site:` (`og:site_name` /
120
121
  `application-name`) lines are added when declared, so the model can judge recency and
121
122
  provenance.
123
+ - A direct fetch refused with HTTP 403 is retried through `web_render` automatically when that
124
+ tool is enabled; the rendered page is prefixed with a note saying so. When `web_render` is
125
+ disabled or the render also fails, the original `Failed to fetch URL: HTTP 403 ...` is returned.
122
126
 
123
127
  ### web_render
124
128
 
@@ -137,6 +141,7 @@ that need JavaScript to render:
137
141
  [Companion: rotating exit IPs](#companion-rotating-exit-ips).
138
142
  - Enabled by default. Disable it with `/webtools-config` (`webRenderEnabled` in
139
143
  `~/.config/pi-unsloth-webtools/config.json`), which deactivates the tool for the session.
144
+ - Also used automatically when a `web_fetch` or `web_search` url-mode fetch is refused with HTTP 403.
140
145
 
141
146
  ## Known differences from Studio
142
147
 
@@ -148,6 +153,8 @@ that need JavaScript to render:
148
153
  - Third-party rendering: the extra `web_render` tool asks the Jina Reader (`r.jina.ai`) to fetch
149
154
  the page, so the target URL leaves the machine. Studio has no third-party rendering path. This
150
155
  path always refuses local files and non-public addresses, regardless of the local-access settings.
156
+ A direct fetch refused with HTTP 403 is retried through it automatically when enabled, so those
157
+ targets also leave the machine in that case.
151
158
  - PDF styling: MuPDF.js exposes one font per line, so mixed-style lines style the
152
159
  whole line instead of per-span; superscript, subscript, underline, strikeout, and
153
160
  highlight markers are not emitted. Tables use a conservative text-grid detector:
@@ -260,7 +267,7 @@ first changed:
260
267
 
261
268
  | Key | Default | Description |
262
269
  |---|---|---|
263
- | `webRenderEnabled` | `true` | When `false`, the `web_render` tool is deactivated for the session; `web_search` and `web_fetch` are unaffected |
270
+ | `webRenderEnabled` | `true` | When `false`, the `web_render` tool is deactivated for the session and the automatic HTTP 403 fallback in `web_fetch` / `web_search` url mode is disabled |
264
271
 
265
272
  On non-Windows platforms the directory honors `XDG_CONFIG_HOME` when set (falling back to
266
273
  `~/.config`); on Windows it always uses `~/.config`.
@@ -284,7 +291,7 @@ Match on the exact prefix. Do not retry blocked hosts with spelling tricks.
284
291
  | Private address blocked | `Blocked: refusing to fetch the non-public address ...` | The SSRF guard is active (`allowPrivateAddresses: false`); remove it or set `true` to reach localhost/LAN, and write the scheme explicitly (`http://localhost:3000`). |
285
292
  | Local file blocked | `Blocked: the URL has an invalid hostname or port.` for paths | Local files are disabled: remove `allowLocalFiles: false` to read `file://`, absolute, `~/`, or `./` paths. |
286
293
  | File read failed | `Failed to read file: ...` | Check the path exists and is a regular file. |
287
- | HTTP failure | `Failed to fetch URL: HTTP ...` | Fix the URL. A 404 automatically tries a Wayback snapshot. |
294
+ | HTTP failure | `Failed to fetch URL: HTTP ...` | Fix the URL. A 404 automatically tries a Wayback snapshot; a 403 retries through `web_render` when enabled. |
288
295
  | Non-text / binary | `(non-text content:` / `(binary content,` | Not readable as text by design. |
289
296
  | PDF without text | `(PDF contains no extractable text)` / `(PDF content could not be read as text...)` | Scanned or encrypted PDF. |
290
297
  | Download cap hit | `... (page truncated at the download limit)` | Raw fetch hit 512 KiB (10 MiB for PDFs). |
package/index.ts CHANGED
@@ -6,7 +6,7 @@ import { SEARCH_TIMEOUT_MS, webSearch as defaultWebSearch } from "./web-search.t
6
6
  import { DEFAULT_FETCH_TIMEOUT_MS, fetchPageText as defaultFetchPageText } from "./web-fetch.ts";
7
7
  import { renderPageText as defaultRenderPageText } from "./web-render.ts";
8
8
  import { loadDefaultFetchSettings, loadDefaultFetchTimeoutMs, loadJinaApiKey } from "./settings.ts";
9
- import { readConfigWithStatus, toggleWebRender } from "./config.ts";
9
+ import { readConfig, readConfigWithStatus, toggleWebRender } from "./config.ts";
10
10
  import { WebToolsConfigOverlay } from "./config-ui.ts";
11
11
 
12
12
  function toolCallLine(theme: Theme, name: string, detail: string) {
@@ -35,6 +35,12 @@ async function fetchDefaults(cwd: string | undefined, params: { timeoutMs?: unkn
35
35
  };
36
36
  }
37
37
 
38
+ function fetchWasForbidden(text: string): boolean {
39
+ return /^Failed to fetch URL: HTTP 403\b/m.test(text);
40
+ }
41
+
42
+ const FORBIDDEN_FALLBACK_NOTE = "Direct fetch failed with HTTP 403; rendered via the Jina Reader instead.";
43
+
38
44
  const WebSearchParams = Type.Object({
39
45
  query: Type.Optional(
40
46
  Type.String({ description: "The search query" }),
@@ -100,19 +106,46 @@ export interface WebToolsDeps {
100
106
  fetchPageText?: typeof defaultFetchPageText;
101
107
  webSearch?: typeof defaultWebSearch;
102
108
  renderPageText?: typeof defaultRenderPageText;
109
+ webRenderEnabled?: () => Promise<boolean>;
103
110
  }
104
111
 
105
112
  export function createWebTools(deps: WebToolsDeps = {}) {
106
113
  const fetchPageText = deps.fetchPageText ?? defaultFetchPageText;
107
114
  const webSearch = deps.webSearch ?? defaultWebSearch;
108
115
  const renderPageText = deps.renderPageText ?? defaultRenderPageText;
116
+ const webRenderEnabled =
117
+ deps.webRenderEnabled ?? (async () => (await readConfig()).webRenderEnabled !== false);
118
+
119
+ const renderOnForbidden = async (
120
+ url: string,
121
+ failedText: string,
122
+ options: { timeoutMs: number; maxChars?: number; signal?: AbortSignal; cwd?: string },
123
+ ): Promise<string | null> => {
124
+ if (!fetchWasForbidden(failedText)) return null;
125
+ try {
126
+ if (!(await webRenderEnabled())) return null;
127
+ const apiKey = await loadJinaApiKey(options.cwd);
128
+ const rendered = await renderPageText(url, {
129
+ timeoutMs: options.timeoutMs,
130
+ maxChars: options.maxChars,
131
+ signal: options.signal,
132
+ apiKey,
133
+ });
134
+ if (rendered.startsWith("Failed to render URL:") || rendered.startsWith("Blocked:")) return null;
135
+ return FORBIDDEN_FALLBACK_NOTE + "\n\n" + rendered;
136
+ } catch {
137
+ return null;
138
+ }
139
+ };
140
+
109
141
  return {
110
142
  webSearchTool: defineTool({
111
143
  name: "web_search",
112
144
  label: "Web Search",
113
145
  description:
114
146
  "Search the web and fetch page content. Returns snippets for all results. " +
115
- "Use the url parameter to fetch full page text from a specific URL.",
147
+ "Use the url parameter to fetch full page text from a specific URL. " +
148
+ "A direct fetch refused with HTTP 403 falls back to web_render when that tool is enabled.",
116
149
  promptSnippet: "Search the web and fetch page content",
117
150
  promptGuidelines: [
118
151
  'Use web_search with the url parameter (e.g. {"url": "<URL>"}) to read the full text of a page found in search results.',
@@ -129,21 +162,20 @@ export function createWebTools(deps: WebToolsDeps = {}) {
129
162
  onUpdate?.({ content: [{ type: "text", text: `Fetching ${url}...` }], details: {} });
130
163
  const cwd = (_ctx as ExtensionContext | undefined)?.cwd;
131
164
  const { timeoutMs, maxChars, allowPrivateAddresses, allowLocalFiles } = await fetchDefaults(cwd, params);
132
- return {
133
- content: [
134
- {
135
- type: "text",
136
- text: await fetchPageText(url, {
137
- timeoutMs,
138
- signal: signal ?? undefined,
139
- maxChars,
140
- allowPrivateAddresses,
141
- allowLocalFiles,
142
- }),
143
- },
144
- ],
145
- details: {},
146
- };
165
+ const text = await fetchPageText(url, {
166
+ timeoutMs,
167
+ signal: signal ?? undefined,
168
+ maxChars,
169
+ allowPrivateAddresses,
170
+ allowLocalFiles,
171
+ });
172
+ const rendered = await renderOnForbidden(url, text, {
173
+ timeoutMs,
174
+ maxChars,
175
+ signal: signal ?? undefined,
176
+ cwd,
177
+ });
178
+ return { content: [{ type: "text", text: rendered ?? text }], details: {} };
147
179
  }
148
180
  onUpdate?.({ content: [{ type: "text", text: "Searching the web..." }], details: {} });
149
181
  const timeoutParam = positiveNumber(params.timeoutMs);
@@ -168,7 +200,11 @@ export function createWebTools(deps: WebToolsDeps = {}) {
168
200
  "README API, so the README is returned instead of the repo page's UI chrome. " +
169
201
  "Private/loopback/link-local targets and local files (file:// URLs, absolute, ~/ or ./ paths, including " +
170
202
  "PDFs) are supported by default; opt out with webFetch.allowPrivateAddresses: false or " +
171
- "webFetch.allowLocalFiles: false in settings. The download size is capped.",
203
+ "webFetch.allowLocalFiles: false in settings. The download size is capped. " +
204
+ "A direct fetch refused with HTTP 403 falls back to web_render (Jina Reader) when that tool is enabled.",
205
+ promptGuidelines: [
206
+ "web_fetch automatically retries HTTP 403 responses through web_render (Jina Reader); do not call web_render again for the same URL after a 403.",
207
+ ],
172
208
  promptSnippet: "Fetch a web page and return readable text content",
173
209
  parameters: WebFetchParams,
174
210
  renderCall(args, theme) {
@@ -185,7 +221,13 @@ export function createWebTools(deps: WebToolsDeps = {}) {
185
221
  allowPrivateAddresses,
186
222
  allowLocalFiles,
187
223
  });
188
- return { content: [{ type: "text", text }], details: {} };
224
+ const rendered = await renderOnForbidden(params.url, text, {
225
+ timeoutMs,
226
+ maxChars,
227
+ signal: signal ?? undefined,
228
+ cwd,
229
+ });
230
+ return { content: [{ type: "text", text: rendered ?? text }], details: {} };
189
231
  },
190
232
  }),
191
233
  webRenderTool: defineTool({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-unsloth-webtools",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "type": "module",
5
5
  "description": "Pi extension: web_search and web_fetch tools that began as a port of the Unsloth Studio codebase and now diverge from it (multi-engine search, opt-in SSRF guard, HTML-to-Markdown extraction)",
6
6
  "main": "index.ts",