@bacnh85/pi-web 0.3.0 → 0.4.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 +171 -77
- package/extensions/.mocharc.yml +5 -0
- package/extensions/index.ts +474 -0
- package/extensions/lib/brave.ts +45 -0
- package/{lib → extensions/lib}/config.ts +8 -10
- package/{lib → extensions/lib}/crawl4ai.ts +11 -45
- package/extensions/lib/extract.ts +185 -0
- package/{lib → extensions/lib}/firecrawl.ts +7 -19
- package/{lib → extensions/lib}/format.ts +2 -71
- package/extensions/lib/retry.ts +29 -0
- package/extensions/lib/search.ts +240 -0
- package/extensions/lib/searxng.ts +69 -0
- package/extensions/package.json +3 -0
- package/extensions/test/unit/config.test.ts +309 -0
- package/extensions/test/unit/crawl4ai.test.ts +69 -0
- package/extensions/test/unit/extract.test.ts +134 -0
- package/extensions/test/unit/format.test.ts +194 -0
- package/extensions/test/unit/search.test.ts +147 -0
- package/package.json +11 -10
- package/skills/pi-web/SKILL.md +108 -0
- package/index.ts +0 -638
- package/lib/brave.ts +0 -40
- package/lib/retry.ts +0 -142
- package/lib/searxng.ts +0 -63
- /package/{lib → extensions/lib}/content.ts +0 -0
- /package/{types.d.ts → extensions/types.d.ts} +0 -0
package/lib/brave.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
// Brave Search API client.
|
|
2
|
-
|
|
3
|
-
import { sanitizeSnippet, type SearchResultItem } from "./format";
|
|
4
|
-
import { withRetry } from "./retry";
|
|
5
|
-
|
|
6
|
-
export interface BraveResult extends SearchResultItem {
|
|
7
|
-
age: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export async function fetchBraveResults(
|
|
11
|
-
query: string,
|
|
12
|
-
count: number,
|
|
13
|
-
country: string,
|
|
14
|
-
freshness: string,
|
|
15
|
-
apiKey: string,
|
|
16
|
-
signal?: AbortSignal,
|
|
17
|
-
): Promise<BraveResult[]> {
|
|
18
|
-
return withRetry(async () => {
|
|
19
|
-
const params = new URLSearchParams({ q: query, count: String(count), country });
|
|
20
|
-
if (freshness) params.append("freshness", freshness);
|
|
21
|
-
const response = await fetch(`https://api.search.brave.com/res/v1/web/search?${params}`, {
|
|
22
|
-
headers: {
|
|
23
|
-
Accept: "application/json",
|
|
24
|
-
"Accept-Encoding": "gzip",
|
|
25
|
-
"X-Subscription-Token": apiKey,
|
|
26
|
-
},
|
|
27
|
-
signal,
|
|
28
|
-
});
|
|
29
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}\n${await response.text()}`);
|
|
30
|
-
const data: any = await response.json();
|
|
31
|
-
return (data.web?.results || [])
|
|
32
|
-
.slice(0, count)
|
|
33
|
-
.map((r: any) => ({
|
|
34
|
-
title: sanitizeSnippet(r.title || ""),
|
|
35
|
-
url: r.url || "",
|
|
36
|
-
snippet: sanitizeSnippet(r.description || ""),
|
|
37
|
-
age: sanitizeSnippet(r.age || r.page_age || ""),
|
|
38
|
-
}));
|
|
39
|
-
}, { maxRetries: 2, retryableErrors: ["timeout", "econn", "etimedout", "network", "socket"] });
|
|
40
|
-
}
|
package/lib/retry.ts
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
// Retry logic with exponential backoff for transient network failures.
|
|
2
|
-
// Ported from pi-munin/lib/retry.ts.
|
|
3
|
-
|
|
4
|
-
export interface RetryOptions {
|
|
5
|
-
maxRetries?: number;
|
|
6
|
-
initialDelay?: number;
|
|
7
|
-
maxDelay?: number;
|
|
8
|
-
multiplier?: number;
|
|
9
|
-
retryableErrors?: string[];
|
|
10
|
-
onRetry?: (attempt: number, max: number, delay: number, error: Error) => void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function isRetryableError(error: Error): boolean {
|
|
14
|
-
const errorMessage = (error.message || "").toLowerCase();
|
|
15
|
-
const retryablePatterns = [
|
|
16
|
-
"econnrefused",
|
|
17
|
-
"enotfound",
|
|
18
|
-
"etimedout",
|
|
19
|
-
"econnreset",
|
|
20
|
-
"econnaborted",
|
|
21
|
-
"enetunreach",
|
|
22
|
-
"etimeout",
|
|
23
|
-
"timeout",
|
|
24
|
-
"network",
|
|
25
|
-
"socket hang up",
|
|
26
|
-
"socket",
|
|
27
|
-
"connection",
|
|
28
|
-
"fetch",
|
|
29
|
-
"request timeout",
|
|
30
|
-
];
|
|
31
|
-
return retryablePatterns.some((pattern) => errorMessage.includes(pattern));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function sleep(ms: number): Promise<void> {
|
|
35
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function calculateDelay(
|
|
39
|
-
attempt: number,
|
|
40
|
-
initialDelay: number,
|
|
41
|
-
maxDelay: number,
|
|
42
|
-
multiplier: number,
|
|
43
|
-
): number {
|
|
44
|
-
const exponentialDelay = initialDelay * Math.pow(multiplier, attempt);
|
|
45
|
-
const jitter = Math.random() * 0.1 * exponentialDelay;
|
|
46
|
-
return Math.min(exponentialDelay + jitter, maxDelay);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Retry a function with exponential backoff.
|
|
51
|
-
* Never retries auth, not_found, or validation errors.
|
|
52
|
-
*/
|
|
53
|
-
export async function withRetry<T>(
|
|
54
|
-
fn: () => Promise<T>,
|
|
55
|
-
options: RetryOptions = {},
|
|
56
|
-
): Promise<T> {
|
|
57
|
-
const {
|
|
58
|
-
maxRetries = 3,
|
|
59
|
-
initialDelay = 1000,
|
|
60
|
-
maxDelay = 10000,
|
|
61
|
-
multiplier = 2,
|
|
62
|
-
retryableErrors = [],
|
|
63
|
-
onRetry,
|
|
64
|
-
} = options;
|
|
65
|
-
|
|
66
|
-
// Errors we never retry (non-transient)
|
|
67
|
-
const nonRetryablePatterns = [
|
|
68
|
-
"unauthorized",
|
|
69
|
-
"invalid api key",
|
|
70
|
-
"not found",
|
|
71
|
-
"validation",
|
|
72
|
-
"required",
|
|
73
|
-
"refusing to send",
|
|
74
|
-
"refusing to send a configured",
|
|
75
|
-
"must use http or https",
|
|
76
|
-
"could not extract readable content",
|
|
77
|
-
"brave_api_key is required",
|
|
78
|
-
"firecrawl_api_key is required",
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
let lastError: Error;
|
|
82
|
-
|
|
83
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
84
|
-
try {
|
|
85
|
-
return await fn();
|
|
86
|
-
} catch (error) {
|
|
87
|
-
lastError = error as Error;
|
|
88
|
-
|
|
89
|
-
if (attempt === maxRetries) throw error;
|
|
90
|
-
|
|
91
|
-
// Never retry non-transient errors
|
|
92
|
-
const msg = lastError.message.toLowerCase();
|
|
93
|
-
if (nonRetryablePatterns.some((p) => msg.includes(p))) throw error;
|
|
94
|
-
|
|
95
|
-
// Check if error is retryable
|
|
96
|
-
const shouldRetry =
|
|
97
|
-
retryableErrors.length > 0
|
|
98
|
-
? retryableErrors.some((pattern) => msg.includes(pattern.toLowerCase()))
|
|
99
|
-
: isRetryableError(lastError);
|
|
100
|
-
|
|
101
|
-
if (!shouldRetry) throw error;
|
|
102
|
-
|
|
103
|
-
const delay = calculateDelay(attempt, initialDelay, maxDelay, multiplier);
|
|
104
|
-
|
|
105
|
-
if (onRetry) {
|
|
106
|
-
onRetry(attempt + 1, maxRetries + 1, delay, lastError);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
await sleep(delay);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
throw lastError!;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Combine a timeout signal with an optional external signal.
|
|
118
|
-
*/
|
|
119
|
-
export function signalWithTimeout(timeoutMs: number, signal?: AbortSignal): AbortSignal {
|
|
120
|
-
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
121
|
-
if (!signal) return timeoutSignal;
|
|
122
|
-
return AbortSignal.any([signal, timeoutSignal]);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Sleep that is abortable via an AbortSignal.
|
|
127
|
-
*/
|
|
128
|
-
export function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
129
|
-
if (signal?.aborted) return Promise.reject(signal.reason ?? new Error("Operation aborted."));
|
|
130
|
-
return new Promise((resolve, reject) => {
|
|
131
|
-
const timer = setTimeout(cleanupResolve, ms);
|
|
132
|
-
function cleanupResolve() {
|
|
133
|
-
signal?.removeEventListener("abort", cleanupReject);
|
|
134
|
-
resolve();
|
|
135
|
-
}
|
|
136
|
-
function cleanupReject() {
|
|
137
|
-
clearTimeout(timer);
|
|
138
|
-
reject(signal?.reason ?? new Error("Operation aborted."));
|
|
139
|
-
}
|
|
140
|
-
signal?.addEventListener("abort", cleanupReject, { once: true });
|
|
141
|
-
});
|
|
142
|
-
}
|
package/lib/searxng.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
// SearXNG metasearch client.
|
|
2
|
-
|
|
3
|
-
import { sanitizeSnippet } from "./format";
|
|
4
|
-
import { signalWithTimeout, withRetry } from "./retry";
|
|
5
|
-
|
|
6
|
-
export interface SearxngResultItem {
|
|
7
|
-
title: string;
|
|
8
|
-
url: string;
|
|
9
|
-
snippet: string;
|
|
10
|
-
engine: string;
|
|
11
|
-
category: string;
|
|
12
|
-
score?: number;
|
|
13
|
-
publishedDate: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface SearxngResponse {
|
|
17
|
-
results: SearxngResultItem[];
|
|
18
|
-
[key: string]: unknown;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function fetchSearxngResults(
|
|
22
|
-
params: Record<string, unknown>,
|
|
23
|
-
baseUrl: string,
|
|
24
|
-
signal?: AbortSignal,
|
|
25
|
-
): Promise<SearxngResponse> {
|
|
26
|
-
return withRetry(async () => {
|
|
27
|
-
const limit = Math.min(50, Math.max(1, (params.count as number) ?? 5));
|
|
28
|
-
const url = new URL(`${baseUrl}/search`);
|
|
29
|
-
url.searchParams.set("q", params.query as string);
|
|
30
|
-
url.searchParams.set("format", "json");
|
|
31
|
-
url.searchParams.set("pageno", String(Math.max(1, (params.pageno as number) ?? 1)));
|
|
32
|
-
for (const key of ["categories", "engines", "language", "time_range", "safesearch"] as const) {
|
|
33
|
-
const val = params[key];
|
|
34
|
-
if (val !== undefined && val !== "") url.searchParams.set(key, String(val));
|
|
35
|
-
}
|
|
36
|
-
const timeoutMs = (params.timeout_ms as number) || 15000;
|
|
37
|
-
const response = await fetch(url, {
|
|
38
|
-
headers: { Accept: "application/json", "User-Agent": "pi-web/0.1 SearXNG" },
|
|
39
|
-
signal: signalWithTimeout(timeoutMs, signal),
|
|
40
|
-
});
|
|
41
|
-
const text = await response.text();
|
|
42
|
-
if (!response.ok) {
|
|
43
|
-
const hint =
|
|
44
|
-
response.status === 403
|
|
45
|
-
? "\nHint: enable JSON output in SearXNG settings.yml with search.formats including json."
|
|
46
|
-
: "";
|
|
47
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}${hint}${text ? `\n${text}` : ""}`);
|
|
48
|
-
}
|
|
49
|
-
const data: any = text ? JSON.parse(text) : {};
|
|
50
|
-
const results = (data.results || [])
|
|
51
|
-
.slice(0, limit)
|
|
52
|
-
.map((r: any) => ({
|
|
53
|
-
title: sanitizeSnippet(r.title || ""),
|
|
54
|
-
url: r.url || "",
|
|
55
|
-
snippet: sanitizeSnippet(r.content || r.snippet || ""),
|
|
56
|
-
engine: r.engine || "",
|
|
57
|
-
category: r.category || "",
|
|
58
|
-
score: r.score,
|
|
59
|
-
publishedDate: r.publishedDate || r.published_date || "",
|
|
60
|
-
}));
|
|
61
|
-
return { ...data, results };
|
|
62
|
-
}, { maxRetries: 2, retryableErrors: ["timeout", "econn", "etimedout", "network", "socket"] });
|
|
63
|
-
}
|
|
File without changes
|
|
File without changes
|