pi-unsloth-webtools 0.4.1 → 0.4.2

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/engines.ts CHANGED
@@ -216,6 +216,10 @@ function parsePredExpr(input: string): Pred {
216
216
  pos += "position()=last()".length;
217
217
  return { op: "last" };
218
218
  }
219
+ if (input.startsWith("last()", pos)) {
220
+ pos += "last()".length;
221
+ return { op: "last" };
222
+ }
219
223
  if (input.startsWith("contains(@class,", pos)) {
220
224
  pos += "contains(@class,".length;
221
225
  const value = quoted();
@@ -359,7 +363,38 @@ function matchesPred(pred: Pred, el: DomNode, index: number, total: number): boo
359
363
  }
360
364
  }
361
365
 
366
+ function predContainsLast(pred: Pred): boolean {
367
+ switch (pred.op) {
368
+ case "last":
369
+ return true;
370
+ case "or":
371
+ case "and":
372
+ return predContainsLast(pred.a) || predContainsLast(pred.b);
373
+ case "child":
374
+ return pred.preds.some(predContainsLast);
375
+ default:
376
+ return false;
377
+ }
378
+ }
379
+
362
380
  function applyStep(step: XStep, nodes: DomNode[]): DomNode[] {
381
+ if (step.preds.some(predContainsLast)) {
382
+ const out: DomNode[] = [];
383
+ const seen = new Set<DomNode>();
384
+ for (const node of nodes) {
385
+ const source = step.axis === "child" ? node.children : descendantsOf(node);
386
+ const list = source.filter((c) => !step.name || c.tag === step.name);
387
+ const total = list.length;
388
+ list.forEach((el, index) => {
389
+ if (seen.has(el)) return;
390
+ if (step.preds.every((p) => matchesPred(p, el, index, total))) {
391
+ seen.add(el);
392
+ out.push(el);
393
+ }
394
+ });
395
+ }
396
+ return out;
397
+ }
363
398
  const candidates: DomNode[] = [];
364
399
  for (const node of nodes) {
365
400
  const list = step.axis === "child" ? node.children : descendantsOf(node);
@@ -427,6 +462,7 @@ export function extractResults(
427
462
  if (!data) continue;
428
463
  result[key] = key === "href" ? normalizeUrl(data) : normalizeText(data);
429
464
  }
465
+ if (!result.title && !result.href && !result.body) continue;
430
466
  results.push(result);
431
467
  }
432
468
  return results;
@@ -445,8 +481,7 @@ function googleUserAgent(): string {
445
481
  return (
446
482
  `Mozilla/5.0 (Linux; Android ${androidVer}; ${device}) ` +
447
483
  `AppleWebKit/537.36 (KHTML, like Gecko) ` +
448
- `Chrome/${chromeMajor}.0.${chromeBuild}.${chromePatch} Mobile Safari/537.36` +
449
- "NSTNWV"
484
+ `Chrome/${chromeMajor}.0.${chromeBuild}.${chromePatch} Mobile Safari/537.36`
450
485
  );
451
486
  }
452
487
 
@@ -794,7 +829,9 @@ export class ResultsAggregator {
794
829
  }
795
830
 
796
831
  append(item: SearchResult): void {
832
+ if (typeof item.href !== "string" || !item.href.trim()) return;
797
833
  const key = canonicalizeHref(item.href);
834
+ if (!key) return;
798
835
  const existing = this.cache.get(key);
799
836
  if (!existing || item.body.length > existing.body.length) {
800
837
  this.cache.set(key, { ...item, href: key });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-unsloth-webtools",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
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",
package/pdf.ts CHANGED
@@ -344,7 +344,7 @@ function findLink(links: LinkInfo[], span: SpanData): string | null {
344
344
  if (midX < link.x0 || midX > link.x1 || midY < link.y0 || midY > link.y1) continue;
345
345
  let uri = link.uri;
346
346
  for (const c of "()\n") {
347
- uri = uri.replaceAll(c, "%0x" + c.charCodeAt(0).toString(16));
347
+ uri = uri.replaceAll(c, "%" + c.charCodeAt(0).toString(16).padStart(2, "0"));
348
348
  }
349
349
  return `[${span.text.trim()}](${uri})`;
350
350
  }
package/web-access.ts CHANGED
@@ -137,7 +137,7 @@ export function normalizeWebsitePolicy(value: unknown): WebsitePolicy {
137
137
  }
138
138
  const normalized: WebsitePolicy = { allowedDomains: [], blockedDomains: [] };
139
139
  for (const key of ["allowedDomains", "blockedDomains"] as const) {
140
- const rawDomains = raw[key];
140
+ const rawDomains = raw[key] ?? [];
141
141
  if (!Array.isArray(rawDomains)) throw new Error(`${key} must be a list`);
142
142
  if (rawDomains.length > MAX_DOMAINS_PER_LIST) {
143
143
  throw new Error(`${key} supports at most ${MAX_DOMAINS_PER_LIST} domains`);
@@ -408,7 +408,7 @@ export function isPublicIp(ip: string): boolean {
408
408
  }
409
409
  if (canonical === "::1" || canonical.startsWith("::")) return false;
410
410
  if (canonical.startsWith("fc") || canonical.startsWith("fd")) return false;
411
- if (/^fe[89ab][0-9a-f]:/.test(canonical)) return false;
411
+ if (/^fe[89a-f][0-9a-f]:/.test(canonical)) return false;
412
412
  if (canonical.startsWith("ff")) return false;
413
413
  if (canonical.startsWith("2001:db8")) return false;
414
414
  if (canonical.startsWith("64:ff9b:")) return false;
package/web-fetch.ts CHANGED
@@ -810,16 +810,22 @@ export async function fetchUrlRaw(
810
810
  const performRequest = seams.request ?? requestHop;
811
811
  const resolveWithBudget = async (hostname: string): Promise<ResolvedHost> => {
812
812
  const abortController = new AbortController();
813
- const timeoutId = setTimeout(() => abortController.abort(), clampedRemainingMs(deadline, now));
813
+ const waitMs = clampedRemainingMs(deadline, now);
814
+ const timeoutId = setTimeout(() => abortController.abort(), waitMs);
814
815
  const resolveSignal = signal ? AbortSignal.any([signal, abortController.signal]) : abortController.signal;
816
+ let deadlineId: ReturnType<typeof setTimeout> | undefined;
817
+ const deadlinePromise = new Promise<ResolvedHost>((resolve) => {
818
+ deadlineId = setTimeout(() => resolve({ ok: false, reason: FETCH_TIMEOUT_MESSAGE, ip: "", family: 0 }), waitMs);
819
+ });
815
820
  try {
816
- const resolved = await resolveHost(hostname, resolveSignal);
821
+ const resolved = await Promise.race([resolveHost(hostname, resolveSignal), deadlinePromise]);
817
822
  if (resolved.ok) return resolved;
818
823
  if (signal?.aborted) return { ...resolved, reason: FETCH_CANCELLED_MESSAGE };
819
824
  if (resolveSignal.aborted) return { ...resolved, reason: FETCH_TIMEOUT_MESSAGE };
820
825
  return resolved;
821
826
  } finally {
822
827
  clearTimeout(timeoutId);
828
+ if (deadlineId !== undefined) clearTimeout(deadlineId);
823
829
  }
824
830
  };
825
831
  const checkBudget = (contentType = ""): RawFetchResult | null => budgetExceededResult(deadline, signal, now, contentType);
package/web-search.ts CHANGED
@@ -23,15 +23,16 @@ export type SearchClient = (
23
23
  query: string,
24
24
  maxResults: number,
25
25
  signal?: AbortSignal,
26
+ timeoutMs?: number,
26
27
  ) => Promise<SearchResult[]>;
27
-
28
28
  export async function ddgSearch(
29
29
  query: string,
30
30
  maxResults = MAX_RESULTS,
31
31
  signal?: AbortSignal,
32
+ timeoutMs = SEARCH_TIMEOUT_MS,
32
33
  ): Promise<SearchResult[]> {
33
34
  if (signal?.aborted) throw new SearchCancelled();
34
- return autoTextSearch(query, maxResults, SEARCH_TIMEOUT_MS, signal);
35
+ return autoTextSearch(query, maxResults, timeoutMs, signal);
35
36
  }
36
37
 
37
38
  const POLICY_OVERFETCH = 4;
@@ -51,8 +52,11 @@ export async function webSearch(
51
52
  query: string | undefined,
52
53
  options: WebSearchOptions = {},
53
54
  ): Promise<string> {
54
- const maxResults = options.maxResults ?? (await loadDefaultMaxResults(options.cwd));
55
- const timeoutMs = options.timeoutMs ?? SEARCH_TIMEOUT_MS;
55
+ const rawMaxResults = options.maxResults ?? (await loadDefaultMaxResults(options.cwd));
56
+ const saneMax = Number.isFinite(rawMaxResults) ? Math.floor(rawMaxResults as number) : MAX_RESULTS;
57
+ const maxResults = Math.min(20, Math.max(1, saneMax));
58
+ const rawTimeout = options.timeoutMs ?? SEARCH_TIMEOUT_MS;
59
+ const timeoutMs = Number.isFinite(rawTimeout) ? (rawTimeout as number) : SEARCH_TIMEOUT_MS;
56
60
  const signal = options.signal;
57
61
  const policy = options.websitePolicy ?? null;
58
62
  const client = options.client ?? ddgSearch;
@@ -66,14 +70,15 @@ export async function webSearch(
66
70
  (policy?.blockedDomains?.length ?? 0) > 0,
67
71
  );
68
72
  const wanted = restricted ? maxResults * POLICY_OVERFETCH : maxResults;
69
- const results = await client(effectiveQuery, wanted, signal);
73
+ const results = await client(effectiveQuery, wanted, signal, timeoutMs);
70
74
  if (signal?.aborted) return "Search cancelled.";
71
75
  if (!results.length) return EMPTY_SEARCH_RESULTS[0];
72
76
  const allowed: SearchResult[] = [];
73
77
  for (const result of results) {
74
78
  if (allowed.length >= maxResults) break;
75
79
  const href = String(result.href ?? "").trim();
76
- if (href && !checkUrlAccess(href, policy)[0]) continue;
80
+ if (!href) continue;
81
+ if (!checkUrlAccess(href, policy)[0]) continue;
77
82
  allowed.push(result);
78
83
  }
79
84
  if (!allowed.length) return EMPTY_SEARCH_RESULTS[1];