pi-unsloth-webtools 0.5.0 → 0.5.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
@@ -174,10 +174,13 @@ Optional settings in `~/.pi/agent/settings.json` or `.pi/settings.json` (project
174
174
  |---|---|---|
175
175
  | `unslothWebTools.maxResults` | `5` | Default `maxResults` for `web_search` (clamped 1-20) |
176
176
  | `unslothWebTools.maxChars` / `webFetch.maxChars` / `smartFetchDefaultMaxChars` | tool param | Default `maxChars` for `web_fetch` and `web_search` url mode |
177
- | `unslothWebTools.timeoutMs` / `webFetch.timeoutMs` / `smartFetchDefaultTimeoutMs` | `60000` | Default `timeoutMs` (>=1000) |
177
+ | `unslothWebTools.timeoutMs` / `webFetch.timeoutMs` / `smartFetchDefaultTimeoutMs` | `60000` fetch, `300000` search | Default `timeoutMs` when the tool param is absent (>=1000). Fetch and `web_search` url mode fall back to 60000; `web_search` query mode falls back to 300000 |
178
178
  | `webSearch.maxResults` / `smartWebSearch.resultsPerQuery` | same as above | Legacy aliases for `maxResults` |
179
+ | `websitePolicy` | none | Not read from settings. Tools run unrestricted by default; `websitePolicy` is a programmatic option the host passes to `webSearch` / `fetchPageText` |
179
180
 
180
- Tool params always win over file defaults.
181
+ Tool params always win over file defaults. Search dedup also strips default ports, so `https://example.com:443/a` and `https://example.com/a` collapse.
182
+
183
+ Environment overrides: `PI_UNSLOTH_CACHE_DIR` changes the fetch cache directory, `PI_UNSLOTH_WEBTOOLS_STATS` opts into append-only sweep stats JSONL, `PI_CODING_AGENT_DIR` / `PI_AGENT_DIR` change the global settings directory. Cache entries live 1 hour and stale copies are served only after a network failure.
181
184
 
182
185
  ## Troubleshooting
183
186
 
package/cache.ts CHANGED
@@ -98,9 +98,17 @@ async function prune(): Promise<void> {
98
98
  } catch {
99
99
  return;
100
100
  }
101
+ const now = Date.now();
102
+ const tmpFiles = files.filter((f) => f.includes(".tmp."));
103
+ for (const file of tmpFiles) {
104
+ try {
105
+ const full = join(dir, file);
106
+ const s = await stat(full);
107
+ if (now - s.mtimeMs >= CACHE_TTL_MS) await unlinkSafe(full);
108
+ } catch {}
109
+ }
101
110
  const jsonFiles = files.filter((f) => f.endsWith(".json"));
102
111
  if (!jsonFiles.length) return;
103
- const now = Date.now();
104
112
  const entries: { file: string; size: number; timestamp: number }[] = [];
105
113
  let total = 0;
106
114
  for (const file of jsonFiles) {
package/engines.ts CHANGED
@@ -82,6 +82,9 @@ export function canonicalizeHref(href: string): string {
82
82
  }
83
83
  }
84
84
  url.hash = "";
85
+ if ((url.protocol === "http:" && url.port === "80") || (url.protocol === "https:" && url.port === "443")) {
86
+ url.port = "";
87
+ }
85
88
  return url.toString();
86
89
  } catch {
87
90
  return href;
package/index.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { defineTool, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
- import { webSearch as defaultWebSearch } from "./web-search.ts";
3
+ import { SEARCH_TIMEOUT_MS, webSearch as defaultWebSearch } from "./web-search.ts";
4
4
  import { DEFAULT_FETCH_TIMEOUT_MS, fetchPageText as defaultFetchPageText } from "./web-fetch.ts";
5
- import { loadDefaultFetchSettings } from "./settings.ts";
5
+ import { loadDefaultFetchSettings, loadDefaultFetchTimeoutMs } from "./settings.ts";
6
6
 
7
7
  function positiveNumber(value: unknown): number | undefined {
8
8
  if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
@@ -107,11 +107,14 @@ export function createWebTools(deps: WebToolsDeps = {}) {
107
107
  };
108
108
  }
109
109
  onUpdate?.({ content: [{ type: "text", text: "Searching the web..." }], details: {} });
110
+ const timeoutParam = positiveNumber(params.timeoutMs);
111
+ const searchCwd = (_ctx as ExtensionContext | undefined)?.cwd;
112
+ const searchTimeoutMs = timeoutParam ?? (await loadDefaultFetchTimeoutMs(searchCwd)) ?? SEARCH_TIMEOUT_MS;
110
113
  const text = await webSearch(params.query, {
111
114
  signal: signal ?? undefined,
112
- timeoutMs: positiveNumber(params.timeoutMs),
115
+ timeoutMs: searchTimeoutMs,
113
116
  maxResults: positiveNumber(params.maxResults),
114
- cwd: (_ctx as ExtensionContext | undefined)?.cwd,
117
+ cwd: searchCwd,
115
118
  });
116
119
  return { content: [{ type: "text", text }], details: {} };
117
120
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-unsloth-webtools",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
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,7 +55,7 @@
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 && npm run check:publint",
58
+ "prepublishOnly": "npm run typecheck && npm run lint && npm run test:unit && npm run check:package && npm run check:publint",
59
59
  "lint": "oxlint .",
60
60
  "check:publint": "publint",
61
61
  "check:attw": "attw --pack"
package/pdf.ts CHANGED
@@ -2,6 +2,7 @@ import { createRequire } from "node:module";
2
2
  import { inflateSync } from "node:zlib";
3
3
 
4
4
  export const MAX_WEB_PDF_PAGES = 50;
5
+ const MAX_PDF_INFLATE_BYTES = 64 * 1024 * 1024;
5
6
 
6
7
  export class PdfParseError extends Error {
7
8
  constructor() {
@@ -786,7 +787,7 @@ async function extractPdfTextFallback(data: Buffer): Promise<string> {
786
787
  let bytes = raw;
787
788
  if (isFlate) {
788
789
  try {
789
- bytes = inflateSync(raw);
790
+ bytes = inflateSync(raw, { maxOutputLength: MAX_PDF_INFLATE_BYTES });
790
791
  } catch {
791
792
  continue;
792
793
  }
package/user-agents.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  const USER_AGENTS = [
2
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
3
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
4
- "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
5
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0",
6
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:133.0) Gecko/20100101 Firefox/133.0",
7
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Safari/605.1.15",
2
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
3
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
4
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
5
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0",
6
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0",
7
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Safari/605.1.15",
8
8
  ];
9
-
10
9
  export function randomUserAgent(): string {
11
10
  return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
12
11
  }
package/web-fetch.ts CHANGED
@@ -631,8 +631,14 @@ export function requestHop(opts: HopOptions): Promise<HopResponse> {
631
631
  headers: opts.headers,
632
632
  timeout: opts.inactivityMs,
633
633
  servername: url.protocol === "https:" ? url.hostname : undefined,
634
- lookup: (_host, _opts, callback) =>
635
- callback(null, [{ address: opts.pinnedIp, family: opts.family }]),
634
+ lookup: ((_hostname: string, _options: unknown, _callback: unknown) => {
635
+ const done = (typeof _options === "function" ? _options : _callback) as (err: unknown, address: unknown, family?: unknown) => void;
636
+ if ((_options as { all?: boolean } | undefined)?.all) {
637
+ done(null, [{ address: opts.pinnedIp, family: opts.family }]);
638
+ } else {
639
+ done(null, opts.pinnedIp, opts.family);
640
+ }
641
+ }) as typeof options.lookup,
636
642
  };
637
643
  let settled = false;
638
644
  let resRef: IncomingMessage | null = null;
@@ -798,7 +804,7 @@ export async function fetchUrlRaw(
798
804
  url: string,
799
805
  options: RawFetchOptions = {},
800
806
  ): Promise<RawFetchResult> {
801
- const timeoutMs = options.timeoutMs ?? 30_000;
807
+ const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
802
808
  const now = options.nowMs ?? Date.now;
803
809
  const deadline = options.deadlineMs ?? now() + timeoutMs;
804
810
  const signal = options.signal;
package/web-search.ts CHANGED
@@ -11,7 +11,7 @@ import {
11
11
 
12
12
  export { EmptySweepError, SearchCancelled, SearchTimeoutError } from "./engines.ts";
13
13
 
14
- const SEARCH_TIMEOUT_MS = 300_000;
14
+ export const SEARCH_TIMEOUT_MS = 300_000;
15
15
  const MAX_RESULTS = 5;
16
16
 
17
17
  export const EMPTY_SEARCH_RESULTS = [
@@ -56,7 +56,8 @@ export async function webSearch(
56
56
  const saneMax = Number.isFinite(rawMaxResults) ? Math.floor(rawMaxResults as number) : MAX_RESULTS;
57
57
  const maxResults = Math.min(20, Math.max(1, saneMax));
58
58
  const rawTimeout = options.timeoutMs ?? SEARCH_TIMEOUT_MS;
59
- const timeoutMs = Number.isFinite(rawTimeout) ? (rawTimeout as number) : SEARCH_TIMEOUT_MS;
59
+ const flooredTimeout = Number.isFinite(rawTimeout) ? Math.floor(rawTimeout as number) : SEARCH_TIMEOUT_MS;
60
+ const timeoutMs = flooredTimeout >= 1 ? flooredTimeout : SEARCH_TIMEOUT_MS;
60
61
  const signal = options.signal;
61
62
  const policy = options.websitePolicy ?? null;
62
63
  const client = options.client ?? ddgSearch;