@bacnh85/pi-web 0.5.2 → 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,42 +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
- let output = lines.slice(0, OUTPUT_MAX_LINES).join("\n");
54
- while (Buffer.byteLength(output, "utf8") > OUTPUT_MAX_BYTES) {
55
- output = output.slice(0, Math.max(0, output.length - 1024));
56
- }
57
- if (lines.length > OUTPUT_MAX_LINES || output.length < text.length) {
58
- output += `\n\n[Web output truncated to ${OUTPUT_MAX_LINES} lines / ${OUTPUT_MAX_BYTES} bytes.]`;
59
- }
60
- return output;
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.]`;
61
58
  }
62
59
 
63
60
  // ---------------------------------------------------------------------------
@@ -65,30 +62,30 @@ export function truncateText(text: string): string {
65
62
  // ---------------------------------------------------------------------------
66
63
 
67
64
  export interface UnifiedSearchResult {
68
- title: string;
69
- url: string;
70
- snippet: string;
71
- age: string;
72
- content: string;
73
- backend: string;
65
+ title: string;
66
+ url: string;
67
+ snippet: string;
68
+ age: string;
69
+ content: string;
70
+ backend: string;
74
71
  }
75
72
 
76
73
  export function formatUnifiedSearchResults(results: UnifiedSearchResult[]): string {
77
- if (!results.length) return "No results found.";
78
- return results
79
- .map((r, i) =>
80
- [
81
- `--- Result ${i + 1} (backend: ${r.backend}) ---`,
82
- `Title: ${r.title || ""}`,
83
- `Link: ${r.url || ""}`,
84
- r.age ? `Age: ${r.age}` : "",
85
- r.snippet ? `Snippet: ${r.snippet}` : "",
86
- r.content ? `Content:\n${r.content}` : "",
87
- ]
88
- .filter(Boolean)
89
- .join("\n"),
90
- )
91
- .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");
92
89
  }
93
90
 
94
91
  // ---------------------------------------------------------------------------
@@ -96,20 +93,20 @@ export function formatUnifiedSearchResults(results: UnifiedSearchResult[]): stri
96
93
  // ---------------------------------------------------------------------------
97
94
 
98
95
  export function formatFirecrawlScrape(data: Record<string, unknown>, maxChars = 20000): string {
99
- const page = (data.data as Record<string, unknown>) || data;
100
- const meta = (page.metadata as Record<string, unknown>) || {};
101
- const parts: string[] = [];
102
- if (meta.title) parts.push(`# ${meta.title}`);
103
- if (meta.sourceURL || meta.url) parts.push(`Source: ${(meta.sourceURL || meta.url) as string}`);
104
- if (meta.statusCode) parts.push(`Status: ${meta.statusCode}`);
105
- const warning = (data.warning || page.warning) as string | undefined;
106
- if (warning) parts.push(`Warning: ${warning}`);
107
- const body = (page.markdown || page.summary || page.answer || page.html || page.rawHtml) as string | undefined;
108
- if (body) parts.push(String(body).slice(0, maxChars));
109
- if (Array.isArray(page.links)) {
110
- parts.push(["## Links", ...(page.links as string[]).slice(0, 100).map((l: string) => `- ${l}`)].join("\n"));
111
- }
112
- 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);
113
110
  }
114
111
 
115
112
  // ---------------------------------------------------------------------------
@@ -117,83 +114,83 @@ export function formatFirecrawlScrape(data: Record<string, unknown>, maxChars =
117
114
  // ---------------------------------------------------------------------------
118
115
 
119
116
  function extractMarkdown(md: unknown): string {
120
- if (!md) return "";
121
- if (typeof md === "string") return md;
122
- if (typeof md === "object") {
123
- const obj = md as Record<string, unknown>;
124
- return (obj.fit_markdown as string) || (obj.raw_markdown as string) || "";
125
- }
126
- 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 "";
127
124
  }
128
125
 
129
126
  function extractLinks(links: unknown): string[] {
130
- if (!links) return [];
131
- const arr = Array.isArray(links) ? links : typeof links === "object" ? Object.values(links) : [];
132
- return arr.flatMap((item: any) => {
133
- if (typeof item === "string") return [item];
134
- if (item?.href) return [item.href];
135
- if (item?.url) return [item.url];
136
- return [];
137
- });
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
+ });
138
135
  }
139
136
 
140
137
  export function formatCrawl4aiResult(data: Record<string, unknown>, maxChars = 20000): string {
141
- // Handle wrapped response: { results: [...] }
142
- const rawResults = (data.results as unknown[]) || [data];
143
- return rawResults
144
- .map((raw: any, i: number) => {
145
- const parts: string[] = [];
146
- const url = raw.url || raw.redirected_url || "";
147
- const success = raw.success !== false;
148
- const statusCode = raw.status_code || raw.metadata?.statusCode;
149
-
150
- if (rawResults.length > 1) parts.push(`=== Result ${i + 1} ===`);
151
- if (url) parts.push(`URL: ${url}`);
152
- if (statusCode) parts.push(`Status: ${statusCode}`);
153
- if (!success) {
154
- parts.push(`Error: ${raw.error_message || "Crawl failed"}`);
155
- return parts.join("\n");
156
- }
157
-
158
- // Markdown content
159
- const md = extractMarkdown(raw.markdown);
160
- if (md) {
161
- const truncated = md.slice(0, maxChars);
162
- parts.push(truncated);
163
- if (truncated.length < md.length) parts.push("[Markdown truncated...]");
164
- } else if (raw.extracted_content) {
165
- const ec =
166
- typeof raw.extracted_content === "string"
167
- ? raw.extracted_content
168
- : JSON.stringify(raw.extracted_content, null, 2);
169
- parts.push(ec.slice(0, maxChars));
170
- } else if (raw.cleaned_html) {
171
- parts.push(`(Cleaned HTML: ${raw.cleaned_html.length} chars)`);
172
- }
173
-
174
- // Links
175
- const links = raw.links as Record<string, unknown> | undefined;
176
- if (links) {
177
- const internalLinks = extractLinks(links.internal);
178
- const externalLinks = extractLinks(links.external);
179
- const allLinks = [...internalLinks, ...externalLinks];
180
- if (allLinks.length) {
181
- parts.push(`Links (${allLinks.length}): ${allLinks.slice(0, 50).join(", ")}`);
182
- }
183
- }
184
-
185
- // Media
186
- const media = raw.media as Record<string, unknown> | undefined;
187
- if (media) {
188
- const imageCount = (media.images as unknown[])?.length || 0;
189
- const videoCount = (media.videos as unknown[])?.length || 0;
190
- if (imageCount || videoCount) {
191
- parts.push(`Media: ${imageCount} images, ${videoCount} videos`);
192
- }
193
- }
194
-
195
- return parts.join("\n\n");
196
- })
197
- .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)";
198
195
  }
199
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
  }