pi-unsloth-webtools 0.4.2 → 0.5.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/README.md +22 -0
- package/engines.ts +26 -9
- package/package.json +10 -4
- package/web-access.ts +2 -0
- package/web-fetch.ts +12 -1
package/README.md
CHANGED
|
@@ -179,6 +179,28 @@ Optional settings in `~/.pi/agent/settings.json` or `.pi/settings.json` (project
|
|
|
179
179
|
|
|
180
180
|
Tool params always win over file defaults.
|
|
181
181
|
|
|
182
|
+
## Troubleshooting
|
|
183
|
+
|
|
184
|
+
Match on the exact prefix. Do not retry blocked hosts with spelling tricks.
|
|
185
|
+
|
|
186
|
+
| Symptom | Exact string | Action |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| Empty query | `No query provided.` | Pass `query` or `url`. |
|
|
189
|
+
| Cancelled | `Search cancelled.` / `Failed to fetch URL: cancelled.` | Retry only if the abort was not intentional. |
|
|
190
|
+
| Search timeout | `Search failed: the search engines (...) did not respond within N seconds.` | Retry, narrow the query, or raise `timeoutMs`. |
|
|
191
|
+
| No results | `No results found.` | Rephrase the query. |
|
|
192
|
+
| Policy filtered everything | `No results found within the website access limits.` | Widen `websitePolicy`, do not work around it. |
|
|
193
|
+
| Blocked URL | `Blocked: ...` | Respect it. Includes non-http schemes, credentials, invalid hosts, non-public IPs, and policy denials. |
|
|
194
|
+
| HTTP failure | `Failed to fetch URL: HTTP ...` | Fix the URL. A 404 automatically tries a Wayback snapshot. |
|
|
195
|
+
| Non-text / binary | `(non-text content:` / `(binary content,` | Not readable as text by design. |
|
|
196
|
+
| PDF without text | `(PDF contains no extractable text)` / `(PDF content could not be read as text...)` | Scanned or encrypted PDF. |
|
|
197
|
+
| Download cap hit | `... (page truncated at the download limit)` | Raw fetch hit 512 KiB (10 MiB for PDFs). |
|
|
198
|
+
| maxChars cut | `... (truncated, N chars total)` | Raise `maxChars` for the full text. |
|
|
199
|
+
| Empty page | `(page returned no readable text)` | Page had no extractable text; JS-rendered pages need a browser tool. |
|
|
200
|
+
| GitHub rewrite | `README of ... (fetched via the GitHub README API):` | Expected repo-root rewrite, not the HTML chrome. |
|
|
201
|
+
| Cache fallback | `Served from cache` / `STALE cache from YYYY-MM-DD` | Network failed; output is the cached copy with its date. |
|
|
202
|
+
| Wayback fallback | `Fetched from Wayback Machine snapshot (YYYY-MM-DD) for ...` | Original 404'd; output is the archived copy with its date. |
|
|
203
|
+
|
|
182
204
|
## Development
|
|
183
205
|
|
|
184
206
|
```sh
|
package/engines.ts
CHANGED
|
@@ -954,9 +954,9 @@ export async function autoTextSearch(
|
|
|
954
954
|
const timedOutProviders = new Set<string>();
|
|
955
955
|
let cancelled = false;
|
|
956
956
|
const uniqueProviders = new Set(engines.map((e) => e.provider)).size;
|
|
957
|
-
const maxWorkers = Math.min(uniqueProviders, Math.ceil(maxResults /
|
|
957
|
+
const maxWorkers = Math.min(uniqueProviders, Math.max(4, Math.ceil(maxResults / 5) + 1));
|
|
958
958
|
let i = 0;
|
|
959
|
-
|
|
959
|
+
const pending = new Set<Promise<void>>();
|
|
960
960
|
const run = async (engine: Engine) => {
|
|
961
961
|
let results: SearchResult[] | null = null;
|
|
962
962
|
for (let attempt = 0; attempt < 2 && results === null; attempt++) {
|
|
@@ -1000,18 +1000,35 @@ export async function autoTextSearch(
|
|
|
1000
1000
|
if (aggregator.size >= maxResults) controller.abort();
|
|
1001
1001
|
}
|
|
1002
1002
|
};
|
|
1003
|
-
while (i < engines.length) {
|
|
1003
|
+
while (i < engines.length || pending.size > 0) {
|
|
1004
1004
|
if (aggregator.size >= maxResults || cancelled) {
|
|
1005
1005
|
controller.abort();
|
|
1006
1006
|
break;
|
|
1007
1007
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1008
|
+
while (i < engines.length && pending.size < maxWorkers) {
|
|
1009
|
+
if (aggregator.size >= maxResults || cancelled) {
|
|
1010
|
+
controller.abort();
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
const engine = engines[i++];
|
|
1014
|
+
if (seenProviders.has(engine.provider)) continue;
|
|
1015
|
+
const task = run(engine);
|
|
1016
|
+
pending.add(task);
|
|
1017
|
+
void task.then(
|
|
1018
|
+
() => {
|
|
1019
|
+
pending.delete(task);
|
|
1020
|
+
},
|
|
1021
|
+
() => {
|
|
1022
|
+
pending.delete(task);
|
|
1023
|
+
},
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
if (pending.size === 0) break;
|
|
1027
|
+
if (aggregator.size >= maxResults || cancelled) {
|
|
1028
|
+
controller.abort();
|
|
1029
|
+
break;
|
|
1014
1030
|
}
|
|
1031
|
+
await Promise.race(pending);
|
|
1015
1032
|
}
|
|
1016
1033
|
await Promise.allSettled(pending);
|
|
1017
1034
|
if (onAbort && signal) signal.removeEventListener("abort", onAbort);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-unsloth-webtools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pi extension: web_search and web_fetch tools ported from the Unsloth Studio codebase (DuckDuckGo search, SSRF-safe direct fetching, HTML-to-Markdown extraction)",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -55,16 +55,22 @@
|
|
|
55
55
|
"test:smoke": "vitest run test/smoke.test.ts",
|
|
56
56
|
"typecheck": "tsc --noEmit",
|
|
57
57
|
"check:package": "node -e \"const fs=require('fs');const pkg=require('./package.json');const listed=new Set(pkg.files);const bad=[];for(const f of pkg.files){if(!fs.existsSync(f))bad.push('missing file: '+f)}for(const f of fs.readdirSync('.').filter(f=>f.endsWith('.ts'))){if(!listed.has(f))bad.push('unlisted source: '+f)}if(bad.length){console.error(bad.join('\\n'));process.exit(1)}\"",
|
|
58
|
-
"prepublishOnly": "npm run typecheck && npm run test:unit && npm run check:package"
|
|
58
|
+
"prepublishOnly": "npm run typecheck && npm run test:unit && npm run check:package && npm run check:publint",
|
|
59
|
+
"lint": "oxlint .",
|
|
60
|
+
"check:publint": "publint",
|
|
61
|
+
"check:attw": "attw --pack"
|
|
59
62
|
},
|
|
60
63
|
"devDependencies": {
|
|
61
64
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
62
65
|
"@types/node": "^24.0.0",
|
|
63
66
|
"typebox": "^1.3.7",
|
|
64
67
|
"typescript": "~5.9.3",
|
|
65
|
-
"vitest": "^4.1.9"
|
|
68
|
+
"vitest": "^4.1.9",
|
|
69
|
+
"@arethetypeswrong/cli": "^0.18.5",
|
|
70
|
+
"oxlint": "^1.81.0",
|
|
71
|
+
"publint": "^0.3.24"
|
|
66
72
|
},
|
|
67
|
-
"
|
|
73
|
+
"optionalDependencies": {
|
|
68
74
|
"mupdf": "^1.28.0"
|
|
69
75
|
}
|
|
70
76
|
}
|
package/web-access.ts
CHANGED
|
@@ -381,6 +381,8 @@ function ipv4Octets(ip: string): number[] | null {
|
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
export function isPublicIp(ip: string): boolean {
|
|
384
|
+
if (ip.includes("%")) return false;
|
|
385
|
+
if (ip.includes(".") && ip.includes(":")) return false;
|
|
384
386
|
if (ip.includes(".")) {
|
|
385
387
|
const o = ipv4Octets(ip);
|
|
386
388
|
if (!o) return false;
|
package/web-fetch.ts
CHANGED
|
@@ -854,7 +854,13 @@ export async function fetchUrlRaw(
|
|
|
854
854
|
Accept: "text/html,application/xhtml+xml,text/plain;q=0.9,*/*;q=0.5",
|
|
855
855
|
"Accept-Encoding": "identity",
|
|
856
856
|
};
|
|
857
|
-
if (options.extraHeaders)
|
|
857
|
+
if (options.extraHeaders) {
|
|
858
|
+
for (const [key, value] of Object.entries(options.extraHeaders)) {
|
|
859
|
+
if (key.toLowerCase() === "host") continue;
|
|
860
|
+
headers[key] = value;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
headers["Host"] = hostHeader;
|
|
858
864
|
const inactivity = remainingMs(deadline, now);
|
|
859
865
|
let response: HopResponse;
|
|
860
866
|
try {
|
|
@@ -1212,6 +1218,11 @@ async function fetchWaybackSnapshot(
|
|
|
1212
1218
|
return null;
|
|
1213
1219
|
}
|
|
1214
1220
|
if (!snapshotUrl) return null;
|
|
1221
|
+
const snapshotAccess = checkUrlAccess(snapshotUrl, options.websitePolicy ?? null);
|
|
1222
|
+
if (!snapshotAccess[0]) return null;
|
|
1223
|
+
const snapshotHost = snapshotAccess[2];
|
|
1224
|
+
const snapshotIsIp = snapshotHost.includes(":") || /^\d+\.\d+\.\d+\.\d+$/.test(snapshotHost);
|
|
1225
|
+
if (snapshotIsIp && !isPublicIp(snapshotHost)) return null;
|
|
1215
1226
|
if (now() >= deadlineMs) return null;
|
|
1216
1227
|
let snapResult: RawFetchResult | null = null;
|
|
1217
1228
|
try {
|