pi-unsloth-webtools 0.7.1 → 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.
- package/README.md +10 -3
- package/index.ts +63 -20
- 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
|
|
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
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { defineTool, type ExtensionAPI, type ExtensionContext, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
4
|
import { collapseWhitespace } from "./html-to-md.ts";
|
|
4
5
|
import { SEARCH_TIMEOUT_MS, webSearch as defaultWebSearch } from "./web-search.ts";
|
|
5
6
|
import { DEFAULT_FETCH_TIMEOUT_MS, fetchPageText as defaultFetchPageText } from "./web-fetch.ts";
|
|
6
7
|
import { renderPageText as defaultRenderPageText } from "./web-render.ts";
|
|
7
8
|
import { loadDefaultFetchSettings, loadDefaultFetchTimeoutMs, loadJinaApiKey } from "./settings.ts";
|
|
8
|
-
import { readConfigWithStatus, toggleWebRender } from "./config.ts";
|
|
9
|
+
import { readConfig, readConfigWithStatus, toggleWebRender } from "./config.ts";
|
|
9
10
|
import { WebToolsConfigOverlay } from "./config-ui.ts";
|
|
10
11
|
|
|
11
12
|
function toolCallLine(theme: Theme, name: string, detail: string) {
|
|
12
13
|
const line = theme.fg("toolTitle", theme.bold(name)) + (detail ? ` ${theme.fg("accent", detail)}` : "");
|
|
13
|
-
return { render: () => [line], invalidate: () => {} };
|
|
14
|
+
return { render: (width: number) => [truncateToWidth(line, width)], invalidate: () => {} };
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
function collapsedArg(value: unknown): string {
|
|
@@ -34,6 +35,12 @@ async function fetchDefaults(cwd: string | undefined, params: { timeoutMs?: unkn
|
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
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
|
+
|
|
37
44
|
const WebSearchParams = Type.Object({
|
|
38
45
|
query: Type.Optional(
|
|
39
46
|
Type.String({ description: "The search query" }),
|
|
@@ -99,19 +106,46 @@ export interface WebToolsDeps {
|
|
|
99
106
|
fetchPageText?: typeof defaultFetchPageText;
|
|
100
107
|
webSearch?: typeof defaultWebSearch;
|
|
101
108
|
renderPageText?: typeof defaultRenderPageText;
|
|
109
|
+
webRenderEnabled?: () => Promise<boolean>;
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
export function createWebTools(deps: WebToolsDeps = {}) {
|
|
105
113
|
const fetchPageText = deps.fetchPageText ?? defaultFetchPageText;
|
|
106
114
|
const webSearch = deps.webSearch ?? defaultWebSearch;
|
|
107
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
|
+
|
|
108
141
|
return {
|
|
109
142
|
webSearchTool: defineTool({
|
|
110
143
|
name: "web_search",
|
|
111
144
|
label: "Web Search",
|
|
112
145
|
description:
|
|
113
146
|
"Search the web and fetch page content. Returns snippets for all results. " +
|
|
114
|
-
"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.",
|
|
115
149
|
promptSnippet: "Search the web and fetch page content",
|
|
116
150
|
promptGuidelines: [
|
|
117
151
|
'Use web_search with the url parameter (e.g. {"url": "<URL>"}) to read the full text of a page found in search results.',
|
|
@@ -128,21 +162,20 @@ export function createWebTools(deps: WebToolsDeps = {}) {
|
|
|
128
162
|
onUpdate?.({ content: [{ type: "text", text: `Fetching ${url}...` }], details: {} });
|
|
129
163
|
const cwd = (_ctx as ExtensionContext | undefined)?.cwd;
|
|
130
164
|
const { timeoutMs, maxChars, allowPrivateAddresses, allowLocalFiles } = await fetchDefaults(cwd, params);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
};
|
|
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: {} };
|
|
146
179
|
}
|
|
147
180
|
onUpdate?.({ content: [{ type: "text", text: "Searching the web..." }], details: {} });
|
|
148
181
|
const timeoutParam = positiveNumber(params.timeoutMs);
|
|
@@ -167,7 +200,11 @@ export function createWebTools(deps: WebToolsDeps = {}) {
|
|
|
167
200
|
"README API, so the README is returned instead of the repo page's UI chrome. " +
|
|
168
201
|
"Private/loopback/link-local targets and local files (file:// URLs, absolute, ~/ or ./ paths, including " +
|
|
169
202
|
"PDFs) are supported by default; opt out with webFetch.allowPrivateAddresses: false or " +
|
|
170
|
-
"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
|
+
],
|
|
171
208
|
promptSnippet: "Fetch a web page and return readable text content",
|
|
172
209
|
parameters: WebFetchParams,
|
|
173
210
|
renderCall(args, theme) {
|
|
@@ -184,7 +221,13 @@ export function createWebTools(deps: WebToolsDeps = {}) {
|
|
|
184
221
|
allowPrivateAddresses,
|
|
185
222
|
allowLocalFiles,
|
|
186
223
|
});
|
|
187
|
-
|
|
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: {} };
|
|
188
231
|
},
|
|
189
232
|
}),
|
|
190
233
|
webRenderTool: defineTool({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-unsloth-webtools",
|
|
3
|
-
"version": "0.7.
|
|
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",
|