@bacnh85/pi-web 0.5.3 → 0.5.4

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.
@@ -8,13 +8,13 @@ const OUTPUT_MAX_LINES = 2_000;
8
8
  // ---------------------------------------------------------------------------
9
9
 
10
10
  export interface SearchResultItem {
11
- title: string;
12
- url: string;
13
- snippet?: string;
14
- age?: string;
15
- description?: string;
16
- content?: string;
17
- markdown?: string;
11
+ title: string;
12
+ url: string;
13
+ snippet?: string;
14
+ age?: string;
15
+ description?: string;
16
+ content?: string;
17
+ markdown?: string;
18
18
  }
19
19
 
20
20
  // ---------------------------------------------------------------------------
@@ -22,39 +22,39 @@ export interface SearchResultItem {
22
22
  // ---------------------------------------------------------------------------
23
23
 
24
24
  export function sanitizeSnippet(text = ""): string {
25
- return text
26
- .replace(/<[^>]*>/g, "")
27
- .replace(/&nbsp;/gi, " ")
28
- .replace(/&amp;/gi, "&")
29
- .replace(/&lt;/gi, "<")
30
- .replace(/&gt;/gi, ">")
31
- .replace(/&quot;/gi, '"')
32
- .replace(/&#39;|&#x27;/gi, "'")
33
- .replace(/&#(\d+);/g, (_: string, code: string) => String.fromCodePoint(Number(code)))
34
- .replace(/&#x([0-9a-f]+);/gi, (_: string, code: string) => String.fromCodePoint(Number.parseInt(code, 16)))
35
- .replace(/\s+/g, " ")
36
- .trim();
25
+ return text
26
+ .replace(/<[^>]*>/g, "")
27
+ .replace(/&nbsp;/gi, " ")
28
+ .replace(/&amp;/gi, "&")
29
+ .replace(/&lt;/gi, "<")
30
+ .replace(/&gt;/gi, ">")
31
+ .replace(/&quot;/gi, '"')
32
+ .replace(/&#39;|&#x27;/gi, "'")
33
+ .replace(/&#(\d+);/g, (_: string, code: string) => String.fromCodePoint(Number(code)))
34
+ .replace(/&#x([0-9a-f]+);/gi, (_: string, code: string) => String.fromCodePoint(Number.parseInt(code, 16)))
35
+ .replace(/\s+/g, " ")
36
+ .trim();
37
37
  }
38
38
 
39
39
  /**
40
40
  * Sanitize an error message — redact sensitive tokens and truncate length.
41
41
  */
42
42
  export function sanitizeError(error: unknown): string {
43
- const raw = error instanceof Error ? error.message : String(error);
44
- return raw
45
- .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]")
46
- .replace(/X-Subscription-Token\s*[:=]\s*[^\s]+/gi, "X-Subscription-Token: [REDACTED]")
47
- .replace(/api[_-]?key[=:][^\s&]+/gi, "api_key=[REDACTED]")
48
- .slice(0, 500);
43
+ const raw = error instanceof Error ? error.message : String(error);
44
+ return raw
45
+ .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]")
46
+ .replace(/X-Subscription-Token\s*[:=]\s*[^\s]+/gi, "X-Subscription-Token: [REDACTED]")
47
+ .replace(/api[_-]?key[=:][^\s&]+/gi, "api_key=[REDACTED]")
48
+ .slice(0, 500);
49
49
  }
50
50
 
51
51
  export function truncateText(text: string): string {
52
- const lines = text.split("\n");
53
- const lineTruncated = lines.slice(0, OUTPUT_MAX_LINES).join("\n");
54
- const buf = Buffer.from(lineTruncated, "utf8");
55
- if (buf.length <= OUTPUT_MAX_BYTES && lines.length <= OUTPUT_MAX_LINES) return text;
56
- const truncated = buf.slice(0, OUTPUT_MAX_BYTES).toString("utf8").replace(/\uFFFD+$/g, "");
57
- return truncated + `\n\n[Web output truncated to ${OUTPUT_MAX_LINES} lines / ${OUTPUT_MAX_BYTES} bytes.]`;
52
+ const lines = text.split("\n");
53
+ const lineTruncated = lines.slice(0, OUTPUT_MAX_LINES).join("\n");
54
+ const buf = Buffer.from(lineTruncated, "utf8");
55
+ if (buf.length <= OUTPUT_MAX_BYTES && lines.length <= OUTPUT_MAX_LINES) return text;
56
+ const truncated = buf.slice(0, OUTPUT_MAX_BYTES).toString("utf8").replace(/\uFFFD+$/g, "");
57
+ return truncated + `\n\n[Web output truncated to ${OUTPUT_MAX_LINES} lines / ${OUTPUT_MAX_BYTES} bytes.]`;
58
58
  }
59
59
 
60
60
  // ---------------------------------------------------------------------------
@@ -62,30 +62,30 @@ export function truncateText(text: string): string {
62
62
  // ---------------------------------------------------------------------------
63
63
 
64
64
  export interface UnifiedSearchResult {
65
- title: string;
66
- url: string;
67
- snippet: string;
68
- age: string;
69
- content: string;
70
- backend: string;
65
+ title: string;
66
+ url: string;
67
+ snippet: string;
68
+ age: string;
69
+ content: string;
70
+ backend: string;
71
71
  }
72
72
 
73
73
  export function formatUnifiedSearchResults(results: UnifiedSearchResult[]): string {
74
- if (!results.length) return "No results found.";
75
- return results
76
- .map((r, i) =>
77
- [
78
- `--- Result ${i + 1} (backend: ${r.backend}) ---`,
79
- `Title: ${r.title || ""}`,
80
- `Link: ${r.url || ""}`,
81
- r.age ? `Age: ${r.age}` : "",
82
- r.snippet ? `Snippet: ${r.snippet}` : "",
83
- r.content ? `Content:\n${r.content}` : "",
84
- ]
85
- .filter(Boolean)
86
- .join("\n"),
87
- )
88
- .join("\n\n");
74
+ if (!results.length) return "No results found.";
75
+ return results
76
+ .map((r, i) =>
77
+ [
78
+ `--- Result ${i + 1} (backend: ${r.backend}) ---`,
79
+ `Title: ${r.title || ""}`,
80
+ `Link: ${r.url || ""}`,
81
+ r.age ? `Age: ${r.age}` : "",
82
+ r.snippet ? `Snippet: ${r.snippet}` : "",
83
+ r.content ? `Content:\n${r.content}` : "",
84
+ ]
85
+ .filter(Boolean)
86
+ .join("\n"),
87
+ )
88
+ .join("\n\n");
89
89
  }
90
90
 
91
91
  // ---------------------------------------------------------------------------
@@ -93,20 +93,20 @@ export function formatUnifiedSearchResults(results: UnifiedSearchResult[]): stri
93
93
  // ---------------------------------------------------------------------------
94
94
 
95
95
  export function formatFirecrawlScrape(data: Record<string, unknown>, maxChars = 20000): string {
96
- const page = (data.data as Record<string, unknown>) || data;
97
- const meta = (page.metadata as Record<string, unknown>) || {};
98
- const parts: string[] = [];
99
- if (meta.title) parts.push(`# ${meta.title}`);
100
- if (meta.sourceURL || meta.url) parts.push(`Source: ${(meta.sourceURL || meta.url) as string}`);
101
- if (meta.statusCode) parts.push(`Status: ${meta.statusCode}`);
102
- const warning = (data.warning || page.warning) as string | undefined;
103
- if (warning) parts.push(`Warning: ${warning}`);
104
- const body = (page.markdown || page.summary || page.answer || page.html || page.rawHtml) as string | undefined;
105
- if (body) parts.push(String(body).slice(0, maxChars));
106
- if (Array.isArray(page.links)) {
107
- parts.push(["## Links", ...(page.links as string[]).slice(0, 100).map((l: string) => `- ${l}`)].join("\n"));
108
- }
109
- return parts.join("\n\n") || JSON.stringify(data, null, 2);
96
+ const page = (data.data as Record<string, unknown>) || data;
97
+ const meta = (page.metadata as Record<string, unknown>) || {};
98
+ const parts: string[] = [];
99
+ if (meta.title) parts.push(`# ${meta.title}`);
100
+ if (meta.sourceURL || meta.url) parts.push(`Source: ${(meta.sourceURL || meta.url) as string}`);
101
+ if (meta.statusCode) parts.push(`Status: ${meta.statusCode}`);
102
+ const warning = (data.warning || page.warning) as string | undefined;
103
+ if (warning) parts.push(`Warning: ${warning}`);
104
+ const body = (page.markdown || page.summary || page.answer || page.html || page.rawHtml) as string | undefined;
105
+ if (body) parts.push(String(body).slice(0, maxChars));
106
+ if (Array.isArray(page.links)) {
107
+ parts.push(["## Links", ...(page.links as string[]).slice(0, 100).map((l: string) => `- ${l}`)].join("\n"));
108
+ }
109
+ return parts.join("\n\n") || JSON.stringify(data, null, 2);
110
110
  }
111
111
 
112
112
  // ---------------------------------------------------------------------------
@@ -114,83 +114,83 @@ export function formatFirecrawlScrape(data: Record<string, unknown>, maxChars =
114
114
  // ---------------------------------------------------------------------------
115
115
 
116
116
  function extractMarkdown(md: unknown): string {
117
- if (!md) return "";
118
- if (typeof md === "string") return md;
119
- if (typeof md === "object") {
120
- const obj = md as Record<string, unknown>;
121
- return (obj.fit_markdown as string) || (obj.raw_markdown as string) || "";
122
- }
123
- return "";
117
+ if (!md) return "";
118
+ if (typeof md === "string") return md;
119
+ if (typeof md === "object") {
120
+ const obj = md as Record<string, unknown>;
121
+ return (obj.fit_markdown as string) || (obj.raw_markdown as string) || "";
122
+ }
123
+ return "";
124
124
  }
125
125
 
126
126
  function extractLinks(links: unknown): string[] {
127
- if (!links) return [];
128
- const arr = Array.isArray(links) ? links : typeof links === "object" ? Object.values(links) : [];
129
- return arr.flatMap((item: any) => {
130
- if (typeof item === "string") return [item];
131
- if (item?.href) return [item.href];
132
- if (item?.url) return [item.url];
133
- return [];
134
- });
127
+ if (!links) return [];
128
+ const arr = Array.isArray(links) ? links : typeof links === "object" ? Object.values(links) : [];
129
+ return arr.flatMap((item: any) => {
130
+ if (typeof item === "string") return [item];
131
+ if (item?.href) return [item.href];
132
+ if (item?.url) return [item.url];
133
+ return [];
134
+ });
135
135
  }
136
136
 
137
137
  export function formatCrawl4aiResult(data: Record<string, unknown>, maxChars = 20000): string {
138
- // Handle wrapped response: { results: [...] }
139
- const rawResults = (data.results as unknown[]) || [data];
140
- return rawResults
141
- .map((raw: any, i: number) => {
142
- const parts: string[] = [];
143
- const url = raw.url || raw.redirected_url || "";
144
- const success = raw.success !== false;
145
- const statusCode = raw.status_code || raw.metadata?.statusCode;
146
-
147
- if (rawResults.length > 1) parts.push(`=== Result ${i + 1} ===`);
148
- if (url) parts.push(`URL: ${url}`);
149
- if (statusCode) parts.push(`Status: ${statusCode}`);
150
- if (!success) {
151
- parts.push(`Error: ${raw.error_message || "Crawl failed"}`);
152
- return parts.join("\n");
153
- }
154
-
155
- // Markdown content
156
- const md = extractMarkdown(raw.markdown);
157
- if (md) {
158
- const truncated = md.slice(0, maxChars);
159
- parts.push(truncated);
160
- if (truncated.length < md.length) parts.push("[Markdown truncated...]");
161
- } else if (raw.extracted_content) {
162
- const ec =
163
- typeof raw.extracted_content === "string"
164
- ? raw.extracted_content
165
- : JSON.stringify(raw.extracted_content, null, 2);
166
- parts.push(ec.slice(0, maxChars));
167
- } else if (raw.cleaned_html) {
168
- parts.push(`(Cleaned HTML: ${raw.cleaned_html.length} chars)`);
169
- }
170
-
171
- // Links
172
- const links = raw.links as Record<string, unknown> | undefined;
173
- if (links) {
174
- const internalLinks = extractLinks(links.internal);
175
- const externalLinks = extractLinks(links.external);
176
- const allLinks = [...internalLinks, ...externalLinks];
177
- if (allLinks.length) {
178
- parts.push(`Links (${allLinks.length}): ${allLinks.slice(0, 50).join(", ")}`);
179
- }
180
- }
181
-
182
- // Media
183
- const media = raw.media as Record<string, unknown> | undefined;
184
- if (media) {
185
- const imageCount = (media.images as unknown[])?.length || 0;
186
- const videoCount = (media.videos as unknown[])?.length || 0;
187
- if (imageCount || videoCount) {
188
- parts.push(`Media: ${imageCount} images, ${videoCount} videos`);
189
- }
190
- }
191
-
192
- return parts.join("\n\n");
193
- })
194
- .join("\n\n---\n\n") || "(No data)";
138
+ // Handle wrapped response: { results: [...] }
139
+ const rawResults = (data.results as unknown[]) || [data];
140
+ return rawResults
141
+ .map((raw: any, i: number) => {
142
+ const parts: string[] = [];
143
+ const url = raw.url || raw.redirected_url || "";
144
+ const success = raw.success !== false;
145
+ const statusCode = raw.status_code || raw.metadata?.statusCode;
146
+
147
+ if (rawResults.length > 1) parts.push(`=== Result ${i + 1} ===`);
148
+ if (url) parts.push(`URL: ${url}`);
149
+ if (statusCode) parts.push(`Status: ${statusCode}`);
150
+ if (!success) {
151
+ parts.push(`Error: ${raw.error_message || "Crawl failed"}`);
152
+ return parts.join("\n");
153
+ }
154
+
155
+ // Markdown content
156
+ const md = extractMarkdown(raw.markdown);
157
+ if (md) {
158
+ const truncated = md.slice(0, maxChars);
159
+ parts.push(truncated);
160
+ if (truncated.length < md.length) parts.push("[Markdown truncated...]");
161
+ } else if (raw.extracted_content) {
162
+ const ec =
163
+ typeof raw.extracted_content === "string"
164
+ ? raw.extracted_content
165
+ : JSON.stringify(raw.extracted_content, null, 2);
166
+ parts.push(ec.slice(0, maxChars));
167
+ } else if (raw.cleaned_html) {
168
+ parts.push(`(Cleaned HTML: ${raw.cleaned_html.length} chars)`);
169
+ }
170
+
171
+ // Links
172
+ const links = raw.links as Record<string, unknown> | undefined;
173
+ if (links) {
174
+ const internalLinks = extractLinks(links.internal);
175
+ const externalLinks = extractLinks(links.external);
176
+ const allLinks = [...internalLinks, ...externalLinks];
177
+ if (allLinks.length) {
178
+ parts.push(`Links (${allLinks.length}): ${allLinks.slice(0, 50).join(", ")}`);
179
+ }
180
+ }
181
+
182
+ // Media
183
+ const media = raw.media as Record<string, unknown> | undefined;
184
+ if (media) {
185
+ const imageCount = (media.images as unknown[])?.length || 0;
186
+ const videoCount = (media.videos as unknown[])?.length || 0;
187
+ if (imageCount || videoCount) {
188
+ parts.push(`Media: ${imageCount} images, ${videoCount} videos`);
189
+ }
190
+ }
191
+
192
+ return parts.join("\n\n");
193
+ })
194
+ .join("\n\n---\n\n") || "(No data)";
195
195
  }
196
196
 
@@ -5,11 +5,11 @@
5
5
  // ---------------------------------------------------------------------------
6
6
 
7
7
  export class HttpError extends Error {
8
- status: number;
9
- constructor(status: number, statusText: string, text: string) {
10
- super(`HTTP ${status}: ${statusText}${text ? `\n${text}` : ""}`);
11
- this.status = status;
12
- }
8
+ status: number;
9
+ constructor(status: number, statusText: string, text: string) {
10
+ super(`HTTP ${status}: ${statusText}${text ? `\n${text}` : ""}`);
11
+ this.status = status;
12
+ }
13
13
  }
14
14
 
15
15
  // ---------------------------------------------------------------------------
@@ -17,43 +17,43 @@ export class HttpError extends Error {
17
17
  // ---------------------------------------------------------------------------
18
18
 
19
19
  export async function withRetry<T>(fn: () => Promise<T>, attempts = 3, signal?: AbortSignal): Promise<T> {
20
- let last: unknown;
21
- for (let attempt = 0; attempt < attempts; attempt++) {
22
- if (signal?.aborted) throw signal.reason ?? new Error("Operation aborted.");
23
- try { return await fn(); }
24
- catch (error) {
25
- last = error;
26
- if (attempt === attempts - 1) break;
27
- await abortableSleep(1000 * 2 ** attempt, signal);
28
- }
29
- }
30
- throw last ?? new Error("retry failed");
20
+ let last: unknown;
21
+ for (let attempt = 0; attempt < attempts; attempt++) {
22
+ if (signal?.aborted) throw signal.reason ?? new Error("Operation aborted.");
23
+ try { return await fn(); }
24
+ catch (error) {
25
+ last = error;
26
+ if (attempt === attempts - 1) break;
27
+ await abortableSleep(1000 * 2 ** attempt, signal);
28
+ }
29
+ }
30
+ throw last ?? new Error("retry failed");
31
31
  }
32
32
 
33
33
  /**
34
34
  * Combine a timeout signal with an optional external signal.
35
35
  */
36
36
  export function signalWithTimeout(timeoutMs: number, signal?: AbortSignal): AbortSignal {
37
- const timeoutSignal = AbortSignal.timeout(timeoutMs);
38
- if (!signal) return timeoutSignal;
39
- return AbortSignal.any([signal, timeoutSignal]);
37
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
38
+ if (!signal) return timeoutSignal;
39
+ return AbortSignal.any([signal, timeoutSignal]);
40
40
  }
41
41
 
42
42
  /**
43
43
  * Sleep that is abortable via an AbortSignal.
44
44
  */
45
45
  export function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {
46
- if (signal?.aborted) return Promise.reject(signal.reason ?? new Error("Operation aborted."));
47
- return new Promise((resolve, reject) => {
48
- const timer = setTimeout(cleanupResolve, ms);
49
- function cleanupResolve() {
50
- signal?.removeEventListener("abort", cleanupReject);
51
- resolve();
52
- }
53
- function cleanupReject() {
54
- clearTimeout(timer);
55
- reject(signal?.reason ?? new Error("Operation aborted."));
56
- }
57
- signal?.addEventListener("abort", cleanupReject, { once: true });
58
- });
46
+ if (signal?.aborted) return Promise.reject(signal.reason ?? new Error("Operation aborted."));
47
+ return new Promise((resolve, reject) => {
48
+ const timer = setTimeout(cleanupResolve, ms);
49
+ function cleanupResolve() {
50
+ signal?.removeEventListener("abort", cleanupReject);
51
+ resolve();
52
+ }
53
+ function cleanupReject() {
54
+ clearTimeout(timer);
55
+ reject(signal?.reason ?? new Error("Operation aborted."));
56
+ }
57
+ signal?.addEventListener("abort", cleanupReject, { once: true });
58
+ });
59
59
  }