@jcheesepkg/nanobot 0.7.6 → 0.7.7
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web.d.mts","names":[],"sources":["../../../src/agent/tools/web.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"web.d.mts","names":[],"sources":["../../../src/agent/tools/web.ts"],"mappings":";;;;cAwLa,aAAA,SAAsB,IAAA;EAAA,SACxB,IAAA;EAAA,SACA,WAAA;EAAA,SACA,UAAA;;;;;;;;;;;;;;;;UAcD,MAAA;EAAA,QACA,UAAA;cAEI,MAAA;IAAW,MAAA;IAAiB,UAAA;EAAA;EAMlC,OAAA,CAAQ,IAAA,EAAM,MAAA,oBAA0B,OAAA;AAAA;;cAsDnC,YAAA,SAAqB,IAAA;EAAA,SACvB,IAAA;EAAA,SACA,WAAA;EAAA,SAEA,UAAA;;;;;;;;;;;;;;;;;;;UAcD,eAAA;cAEI,MAAA;IAAW,QAAA;EAAA;EAKjB,OAAA,CAAQ,IAAA,EAAM,MAAA,oBAA0B,OAAA;AAAA"}
|
package/dist/agent/tools/web.mjs
CHANGED
|
@@ -1,7 +1,80 @@
|
|
|
1
1
|
import { Tool } from "./base.mjs";
|
|
2
|
+
import { lookup } from "node:dns/promises";
|
|
3
|
+
import { isIP } from "node:net";
|
|
2
4
|
|
|
3
5
|
//#region src/agent/tools/web.ts
|
|
4
6
|
const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_2) AppleWebKit/537.36";
|
|
7
|
+
/** Blocked hostnames that could resolve to private IPs or allow SSRF. */
|
|
8
|
+
const BLOCKED_HOSTNAMES = new Set([
|
|
9
|
+
"localhost",
|
|
10
|
+
"localhost.localdomain",
|
|
11
|
+
"ip6-localhost",
|
|
12
|
+
"ip6-loopback",
|
|
13
|
+
"loopback",
|
|
14
|
+
"broadcasthost",
|
|
15
|
+
"ip6-localnet",
|
|
16
|
+
"ip6-mcastprefix",
|
|
17
|
+
"0.0.0.0",
|
|
18
|
+
"::1",
|
|
19
|
+
"::",
|
|
20
|
+
"metadata.google.internal",
|
|
21
|
+
"metadata",
|
|
22
|
+
"kubernetes.default",
|
|
23
|
+
"kubernetes.default.svc",
|
|
24
|
+
"169.254.169.254"
|
|
25
|
+
]);
|
|
26
|
+
/** Check if an IP address is private/internal (SSRF protection). */
|
|
27
|
+
function isPrivateIP(ip) {
|
|
28
|
+
const ipVersion = isIP(ip);
|
|
29
|
+
if (ipVersion === 0) return true;
|
|
30
|
+
if (ipVersion === 4) {
|
|
31
|
+
const parts = ip.split(".").map(Number);
|
|
32
|
+
const [a, b] = parts;
|
|
33
|
+
if (Number.isNaN(a) || Number.isNaN(b)) return true;
|
|
34
|
+
if (a === 0) return true;
|
|
35
|
+
if (a === 10) return true;
|
|
36
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
37
|
+
if (a === 127) return true;
|
|
38
|
+
if (a === 169 && b === 254) return true;
|
|
39
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
40
|
+
if (a === 192 && b === 0 && parts[2] === 0) return true;
|
|
41
|
+
if (a === 192 && b === 0 && parts[2] === 2) return true;
|
|
42
|
+
if (a === 192 && b === 88 && parts[2] === 99) return true;
|
|
43
|
+
if (a === 192 && b === 168) return true;
|
|
44
|
+
if (a === 198 && b >= 18 && b <= 19) return true;
|
|
45
|
+
if (a === 198 && b === 51 && parts[2] === 100) return true;
|
|
46
|
+
if (a === 203 && b === 0 && parts[2] === 113) return true;
|
|
47
|
+
if (a >= 224 && a <= 239) return true;
|
|
48
|
+
if (a >= 240) return true;
|
|
49
|
+
if (a === 255 && b === 255 && parts[2] === 255 && parts[3] === 255) return true;
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (ipVersion === 6) {
|
|
53
|
+
const lower = ip.toLowerCase();
|
|
54
|
+
if (lower === "::1") return true;
|
|
55
|
+
if (lower.startsWith("::ffff:")) return true;
|
|
56
|
+
if (lower.startsWith("fe80::")) return true;
|
|
57
|
+
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
|
58
|
+
if (lower.startsWith("ff")) return true;
|
|
59
|
+
if (lower === "::" || lower === "0:0:0:0:0:0:0:0") return true;
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
/** Resolve hostname and check if it resolves to a private IP. */
|
|
65
|
+
async function resolvesToPrivateIP(hostname) {
|
|
66
|
+
if (BLOCKED_HOSTNAMES.has(hostname.toLowerCase())) return true;
|
|
67
|
+
if (isIP(hostname) !== 0) return isPrivateIP(hostname);
|
|
68
|
+
if (hostname.endsWith(".local") || hostname.endsWith(".localhost")) return true;
|
|
69
|
+
if (hostname.endsWith(".internal") || hostname.endsWith(".svc.cluster.local")) return true;
|
|
70
|
+
try {
|
|
71
|
+
const result = await lookup(hostname, { all: true });
|
|
72
|
+
for (const addr of result) if (isPrivateIP(addr.address)) return true;
|
|
73
|
+
return false;
|
|
74
|
+
} catch {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
5
78
|
/** Strip HTML tags and decode entities. */
|
|
6
79
|
function stripTags(text) {
|
|
7
80
|
let result = text;
|
|
@@ -15,8 +88,8 @@ function stripTags(text) {
|
|
|
15
88
|
function normalize(text) {
|
|
16
89
|
return text.replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
|
|
17
90
|
}
|
|
18
|
-
/** Validate URL. */
|
|
19
|
-
function validateUrl(url) {
|
|
91
|
+
/** Validate URL (protocol, hostname, SSRF protection). */
|
|
92
|
+
async function validateUrl(url) {
|
|
20
93
|
try {
|
|
21
94
|
const parsed = new URL(url);
|
|
22
95
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return {
|
|
@@ -27,6 +100,14 @@ function validateUrl(url) {
|
|
|
27
100
|
valid: false,
|
|
28
101
|
error: "Missing domain"
|
|
29
102
|
};
|
|
103
|
+
if (parsed.username || parsed.password) return {
|
|
104
|
+
valid: false,
|
|
105
|
+
error: "URLs with credentials are not allowed"
|
|
106
|
+
};
|
|
107
|
+
if (await resolvesToPrivateIP(parsed.hostname)) return {
|
|
108
|
+
valid: false,
|
|
109
|
+
error: `Access to private/internal addresses is blocked (${parsed.hostname})`
|
|
110
|
+
};
|
|
30
111
|
return {
|
|
31
112
|
valid: true,
|
|
32
113
|
error: ""
|
|
@@ -138,23 +219,44 @@ var WebFetchTool = class extends Tool {
|
|
|
138
219
|
const url = String(args.url);
|
|
139
220
|
const extractMode = String(args.extractMode ?? "markdown");
|
|
140
221
|
const maxChars = args.maxChars ? Number(args.maxChars) : this.defaultMaxChars;
|
|
141
|
-
const { valid, error: validationError } = validateUrl(url);
|
|
222
|
+
const { valid, error: validationError } = await validateUrl(url);
|
|
142
223
|
if (!valid) return JSON.stringify({
|
|
143
224
|
error: `URL validation failed: ${validationError}`,
|
|
144
225
|
url
|
|
145
226
|
});
|
|
146
227
|
try {
|
|
147
|
-
|
|
228
|
+
let finalResp = await fetch(url, {
|
|
148
229
|
headers: { "User-Agent": USER_AGENT },
|
|
149
|
-
redirect: "
|
|
230
|
+
redirect: "manual",
|
|
150
231
|
signal: AbortSignal.timeout(3e4)
|
|
151
232
|
});
|
|
152
|
-
|
|
153
|
-
|
|
233
|
+
let redirectCount = 0;
|
|
234
|
+
const maxRedirects = 5;
|
|
235
|
+
while (finalResp.status >= 300 && finalResp.status < 400 && redirectCount < maxRedirects) {
|
|
236
|
+
const location = finalResp.headers.get("location");
|
|
237
|
+
if (!location) break;
|
|
238
|
+
const { valid: redirectValid } = await validateUrl(location);
|
|
239
|
+
if (!redirectValid) return JSON.stringify({
|
|
240
|
+
error: `Redirect to blocked address: ${location}`,
|
|
241
|
+
url
|
|
242
|
+
});
|
|
243
|
+
finalResp = await fetch(location, {
|
|
244
|
+
headers: { "User-Agent": USER_AGENT },
|
|
245
|
+
redirect: "manual",
|
|
246
|
+
signal: AbortSignal.timeout(3e4)
|
|
247
|
+
});
|
|
248
|
+
redirectCount++;
|
|
249
|
+
}
|
|
250
|
+
if (redirectCount >= maxRedirects) return JSON.stringify({
|
|
251
|
+
error: "Too many redirects",
|
|
252
|
+
url
|
|
253
|
+
});
|
|
254
|
+
if (!finalResp.ok) return JSON.stringify({
|
|
255
|
+
error: `HTTP ${finalResp.status}`,
|
|
154
256
|
url
|
|
155
257
|
});
|
|
156
|
-
const contentType =
|
|
157
|
-
const body = await
|
|
258
|
+
const contentType = finalResp.headers.get("content-type") ?? "";
|
|
259
|
+
const body = await finalResp.text();
|
|
158
260
|
let text;
|
|
159
261
|
let extractor;
|
|
160
262
|
if (contentType.includes("application/json")) {
|
|
@@ -175,8 +277,8 @@ var WebFetchTool = class extends Tool {
|
|
|
175
277
|
if (truncated) text = text.slice(0, maxChars);
|
|
176
278
|
return JSON.stringify({
|
|
177
279
|
url,
|
|
178
|
-
finalUrl:
|
|
179
|
-
status:
|
|
280
|
+
finalUrl: finalResp.url,
|
|
281
|
+
status: finalResp.status,
|
|
180
282
|
extractor,
|
|
181
283
|
truncated,
|
|
182
284
|
length: text.length,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web.mjs","names":[],"sources":["../../../src/agent/tools/web.ts"],"sourcesContent":["import { Tool } from \"./base.js\";\n\nconst USER_AGENT =\n \"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_2) AppleWebKit/537.36\";\n\n/** Strip HTML tags and decode entities. */\nfunction stripTags(text: string): string {\n let result = text;\n result = result.replace(/<script[\\s\\S]*?<\\/script>/gi, \"\");\n result = result.replace(/<style[\\s\\S]*?<\\/style>/gi, \"\");\n result = result.replace(/<[^>]+>/g, \"\");\n // Decode common HTML entities\n result = result\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/ /g, \" \");\n return result.trim();\n}\n\n/** Normalize whitespace. */\nfunction normalize(text: string): string {\n return text\n .replace(/[ \\t]+/g, \" \")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n}\n\n/** Validate URL. */\nfunction validateUrl(url: string): { valid: boolean; error: string } {\n try {\n const parsed = new URL(url);\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n return {\n valid: false,\n error: `Only http/https allowed, got '${parsed.protocol}'`,\n };\n }\n if (!parsed.hostname) {\n return { valid: false, error: \"Missing domain\" };\n }\n return { valid: true, error: \"\" };\n } catch (err) {\n return { valid: false, error: String(err) };\n }\n}\n\n/** Convert HTML to basic markdown. */\nfunction htmlToMarkdown(html: string): string {\n let text = html;\n // Links\n text = text.replace(\n /<a\\s+[^>]*href=[\"']([^\"']+)[\"'][^>]*>([\\s\\S]*?)<\\/a>/gi,\n (_m, url, inner) => `[${stripTags(inner)}](${url})`,\n );\n // Headings\n text = text.replace(\n /<h([1-6])[^>]*>([\\s\\S]*?)<\\/h\\1>/gi,\n (_m, level, inner) => `\\n${\"#\".repeat(Number(level))} ${stripTags(inner)}\\n`,\n );\n // List items\n text = text.replace(\n /<li[^>]*>([\\s\\S]*?)<\\/li>/gi,\n (_m, inner) => `\\n- ${stripTags(inner)}`,\n );\n // Block elements\n text = text.replace(/<\\/(p|div|section|article)>/gi, \"\\n\\n\");\n text = text.replace(/<(br|hr)\\s*\\/?>/gi, \"\\n\");\n return normalize(stripTags(text));\n}\n\n/** Search the web using Brave Search API. */\nexport class WebSearchTool extends Tool {\n readonly name = \"web_search\";\n readonly description = \"Search the web. Returns titles, URLs, and snippets.\";\n readonly parameters = {\n type: \"object\",\n properties: {\n query: { type: \"string\", description: \"Search query\" },\n count: {\n type: \"integer\",\n description: \"Results (1-10)\",\n minimum: 1,\n maximum: 10,\n },\n },\n required: [\"query\"],\n };\n\n private apiKey: string;\n private maxResults: number;\n\n constructor(params?: { apiKey?: string; maxResults?: number }) {\n super();\n this.apiKey = params?.apiKey ?? process.env.BRAVE_API_KEY ?? \"\";\n this.maxResults = params?.maxResults ?? 5;\n }\n\n async execute(args: Record<string, unknown>): Promise<string> {\n const query = String(args.query);\n const count = Math.min(\n Math.max(args.count ? Number(args.count) : this.maxResults, 1),\n 10,\n );\n\n if (!this.apiKey) {\n return \"Error: BRAVE_API_KEY not configured\";\n }\n\n try {\n const url = new URL(\"https://api.search.brave.com/res/v1/web/search\");\n url.searchParams.set(\"q\", query);\n url.searchParams.set(\"count\", String(count));\n\n const resp = await fetch(url.toString(), {\n headers: {\n Accept: \"application/json\",\n \"Accept-Encoding\": \"gzip\",\n \"X-Subscription-Token\": this.apiKey,\n },\n signal: AbortSignal.timeout(10000),\n });\n\n if (!resp.ok) {\n return `Error: Search API returned ${resp.status}`;\n }\n\n const data = (await resp.json()) as {\n web?: { results?: Array<{ title?: string; url?: string; description?: string }> };\n };\n const results = data.web?.results ?? [];\n\n if (results.length === 0) {\n return `No results for: ${query}`;\n }\n\n const lines = [`Results for: ${query}\\n`];\n for (let i = 0; i < Math.min(results.length, count); i++) {\n const item = results[i];\n lines.push(`${i + 1}. ${item.title ?? \"\"}\\n ${item.url ?? \"\"}`);\n if (item.description) {\n lines.push(` ${item.description}`);\n }\n }\n return lines.join(\"\\n\");\n } catch (err) {\n return `Error: ${err instanceof Error ? err.message : err}`;\n }\n }\n}\n\n/** Fetch and extract content from a URL. */\nexport class WebFetchTool extends Tool {\n readonly name = \"web_fetch\";\n readonly description =\n \"Fetch URL and extract readable content (HTML -> markdown/text).\";\n readonly parameters = {\n type: \"object\",\n properties: {\n url: { type: \"string\", description: \"URL to fetch\" },\n extractMode: {\n type: \"string\",\n enum: [\"markdown\", \"text\"],\n description: \"Extract mode\",\n },\n maxChars: { type: \"integer\", minimum: 100 },\n },\n required: [\"url\"],\n };\n\n private defaultMaxChars: number;\n\n constructor(params?: { maxChars?: number }) {\n super();\n this.defaultMaxChars = params?.maxChars ?? 50000;\n }\n\n async execute(args: Record<string, unknown>): Promise<string> {\n const url = String(args.url);\n const extractMode = String(args.extractMode ?? \"markdown\");\n const maxChars = args.maxChars\n ? Number(args.maxChars)\n : this.defaultMaxChars;\n\n const { valid, error: validationError } = validateUrl(url);\n if (!valid) {\n return JSON.stringify({\n error: `URL validation failed: ${validationError}`,\n url,\n });\n }\n\n try {\n const resp = await fetch(url, {\n headers: { \"User-Agent\": USER_AGENT },\n redirect: \"follow\",\n signal: AbortSignal.timeout(30000),\n });\n\n if (!resp.ok) {\n return JSON.stringify({\n error: `HTTP ${resp.status}`,\n url,\n });\n }\n\n const contentType = resp.headers.get(\"content-type\") ?? \"\";\n const body = await resp.text();\n let text: string;\n let extractor: string;\n\n if (contentType.includes(\"application/json\")) {\n try {\n text = JSON.stringify(JSON.parse(body), null, 2);\n } catch {\n text = body;\n }\n extractor = \"json\";\n } else if (\n contentType.includes(\"text/html\") ||\n body.slice(0, 256).toLowerCase().startsWith(\"<!doctype\") ||\n body.slice(0, 256).toLowerCase().startsWith(\"<html\")\n ) {\n // Extract readable content from HTML\n text =\n extractMode === \"markdown\"\n ? htmlToMarkdown(body)\n : stripTags(body);\n extractor = \"html\";\n } else {\n text = body;\n extractor = \"raw\";\n }\n\n const truncated = text.length > maxChars;\n if (truncated) {\n text = text.slice(0, maxChars);\n }\n\n return JSON.stringify({\n url,\n finalUrl: resp.url,\n status: resp.status,\n extractor,\n truncated,\n length: text.length,\n text,\n });\n } catch (err) {\n return JSON.stringify({\n error: err instanceof Error ? err.message : String(err),\n url,\n });\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"web.mjs","names":[],"sources":["../../../src/agent/tools/web.ts"],"sourcesContent":["import { Tool } from \"./base.js\";\nimport { lookup } from \"node:dns/promises\";\nimport { isIP } from \"node:net\";\n\nconst USER_AGENT =\n \"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_2) AppleWebKit/537.36\";\n\n/** Blocked hostnames that could resolve to private IPs or allow SSRF. */\nconst BLOCKED_HOSTNAMES = new Set([\n \"localhost\",\n \"localhost.localdomain\",\n \"ip6-localhost\",\n \"ip6-loopback\",\n \"loopback\",\n \"broadcasthost\",\n \"ip6-localnet\",\n \"ip6-mcastprefix\",\n \"0.0.0.0\",\n \"::1\",\n \"::\",\n \"metadata.google.internal\",\n \"metadata\",\n \"kubernetes.default\",\n \"kubernetes.default.svc\",\n \"169.254.169.254\", // AWS/GCP/Azure metadata (also covered by link-local check)\n]);\n\n/** Check if an IP address is private/internal (SSRF protection). */\nfunction isPrivateIP(ip: string): boolean {\n const ipVersion = isIP(ip);\n if (ipVersion === 0) return true;\n\n if (ipVersion === 4) {\n const parts = ip.split(\".\").map(Number);\n const [a, b] = parts;\n\n if (Number.isNaN(a) || Number.isNaN(b)) return true;\n\n if (a === 0) return true;\n if (a === 10) return true;\n if (a === 100 && b >= 64 && b <= 127) return true; // Carrier-grade NAT (RFC 6598)\n if (a === 127) return true;\n if (a === 169 && b === 254) return true;\n if (a === 172 && b >= 16 && b <= 31) return true;\n if (a === 192 && b === 0 && parts[2] === 0) return true;\n if (a === 192 && b === 0 && parts[2] === 2) return true;\n if (a === 192 && b === 88 && parts[2] === 99) return true;\n if (a === 192 && b === 168) return true;\n if (a === 198 && b >= 18 && b <= 19) return true;\n if (a === 198 && b === 51 && parts[2] === 100) return true;\n if (a === 203 && b === 0 && parts[2] === 113) return true;\n if (a >= 224 && a <= 239) return true;\n if (a >= 240) return true;\n if (a === 255 && b === 255 && parts[2] === 255 && parts[3] === 255) return true;\n\n return false;\n }\n\n if (ipVersion === 6) {\n const lower = ip.toLowerCase();\n if (lower === \"::1\") return true;\n if (lower.startsWith(\"::ffff:\")) return true;\n if (lower.startsWith(\"fe80::\")) return true;\n if (lower.startsWith(\"fc\") || lower.startsWith(\"fd\")) return true;\n if (lower.startsWith(\"ff\")) return true;\n if (lower === \"::\" || lower === \"0:0:0:0:0:0:0:0\") return true;\n return false;\n }\n\n return true;\n}\n\n/** Resolve hostname and check if it resolves to a private IP. */\nasync function resolvesToPrivateIP(hostname: string): Promise<boolean> {\n if (BLOCKED_HOSTNAMES.has(hostname.toLowerCase())) {\n return true;\n }\n\n if (isIP(hostname) !== 0) {\n return isPrivateIP(hostname);\n }\n\n if (hostname.endsWith(\".local\") || hostname.endsWith(\".localhost\")) {\n return true;\n }\n\n if (hostname.endsWith(\".internal\") || hostname.endsWith(\".svc.cluster.local\")) {\n return true;\n }\n\n try {\n const result = await lookup(hostname, { all: true });\n for (const addr of result) {\n if (isPrivateIP(addr.address)) {\n return true;\n }\n }\n return false;\n } catch {\n return true;\n }\n}\n\n/** Strip HTML tags and decode entities. */\nfunction stripTags(text: string): string {\n let result = text;\n result = result.replace(/<script[\\s\\S]*?<\\/script>/gi, \"\");\n result = result.replace(/<style[\\s\\S]*?<\\/style>/gi, \"\");\n result = result.replace(/<[^>]+>/g, \"\");\n // Decode common HTML entities\n result = result\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/ /g, \" \");\n return result.trim();\n}\n\n/** Normalize whitespace. */\nfunction normalize(text: string): string {\n return text\n .replace(/[ \\t]+/g, \" \")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n}\n\n/** Validate URL (protocol, hostname, SSRF protection). */\nasync function validateUrl(url: string): Promise<{ valid: boolean; error: string }> {\n try {\n const parsed = new URL(url);\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n return {\n valid: false,\n error: `Only http/https allowed, got '${parsed.protocol}'`,\n };\n }\n if (!parsed.hostname) {\n return { valid: false, error: \"Missing domain\" };\n }\n\n if (parsed.username || parsed.password) {\n return { valid: false, error: \"URLs with credentials are not allowed\" };\n }\n\n if (await resolvesToPrivateIP(parsed.hostname)) {\n return {\n valid: false,\n error: `Access to private/internal addresses is blocked (${parsed.hostname})`,\n };\n }\n\n return { valid: true, error: \"\" };\n } catch (err) {\n return { valid: false, error: String(err) };\n }\n}\n\n/** Convert HTML to basic markdown. */\nfunction htmlToMarkdown(html: string): string {\n let text = html;\n // Links\n text = text.replace(\n /<a\\s+[^>]*href=[\"']([^\"']+)[\"'][^>]*>([\\s\\S]*?)<\\/a>/gi,\n (_m, url, inner) => `[${stripTags(inner)}](${url})`,\n );\n // Headings\n text = text.replace(\n /<h([1-6])[^>]*>([\\s\\S]*?)<\\/h\\1>/gi,\n (_m, level, inner) => `\\n${\"#\".repeat(Number(level))} ${stripTags(inner)}\\n`,\n );\n // List items\n text = text.replace(\n /<li[^>]*>([\\s\\S]*?)<\\/li>/gi,\n (_m, inner) => `\\n- ${stripTags(inner)}`,\n );\n // Block elements\n text = text.replace(/<\\/(p|div|section|article)>/gi, \"\\n\\n\");\n text = text.replace(/<(br|hr)\\s*\\/?>/gi, \"\\n\");\n return normalize(stripTags(text));\n}\n\n/** Search the web using Brave Search API. */\nexport class WebSearchTool extends Tool {\n readonly name = \"web_search\";\n readonly description = \"Search the web. Returns titles, URLs, and snippets.\";\n readonly parameters = {\n type: \"object\",\n properties: {\n query: { type: \"string\", description: \"Search query\" },\n count: {\n type: \"integer\",\n description: \"Results (1-10)\",\n minimum: 1,\n maximum: 10,\n },\n },\n required: [\"query\"],\n };\n\n private apiKey: string;\n private maxResults: number;\n\n constructor(params?: { apiKey?: string; maxResults?: number }) {\n super();\n this.apiKey = params?.apiKey ?? process.env.BRAVE_API_KEY ?? \"\";\n this.maxResults = params?.maxResults ?? 5;\n }\n\n async execute(args: Record<string, unknown>): Promise<string> {\n const query = String(args.query);\n const count = Math.min(\n Math.max(args.count ? Number(args.count) : this.maxResults, 1),\n 10,\n );\n\n if (!this.apiKey) {\n return \"Error: BRAVE_API_KEY not configured\";\n }\n\n try {\n const url = new URL(\"https://api.search.brave.com/res/v1/web/search\");\n url.searchParams.set(\"q\", query);\n url.searchParams.set(\"count\", String(count));\n\n const resp = await fetch(url.toString(), {\n headers: {\n Accept: \"application/json\",\n \"Accept-Encoding\": \"gzip\",\n \"X-Subscription-Token\": this.apiKey,\n },\n signal: AbortSignal.timeout(10000),\n });\n\n if (!resp.ok) {\n return `Error: Search API returned ${resp.status}`;\n }\n\n const data = (await resp.json()) as {\n web?: { results?: Array<{ title?: string; url?: string; description?: string }> };\n };\n const results = data.web?.results ?? [];\n\n if (results.length === 0) {\n return `No results for: ${query}`;\n }\n\n const lines = [`Results for: ${query}\\n`];\n for (let i = 0; i < Math.min(results.length, count); i++) {\n const item = results[i];\n lines.push(`${i + 1}. ${item.title ?? \"\"}\\n ${item.url ?? \"\"}`);\n if (item.description) {\n lines.push(` ${item.description}`);\n }\n }\n return lines.join(\"\\n\");\n } catch (err) {\n return `Error: ${err instanceof Error ? err.message : err}`;\n }\n }\n}\n\n/** Fetch and extract content from a URL. */\nexport class WebFetchTool extends Tool {\n readonly name = \"web_fetch\";\n readonly description =\n \"Fetch URL and extract readable content (HTML -> markdown/text).\";\n readonly parameters = {\n type: \"object\",\n properties: {\n url: { type: \"string\", description: \"URL to fetch\" },\n extractMode: {\n type: \"string\",\n enum: [\"markdown\", \"text\"],\n description: \"Extract mode\",\n },\n maxChars: { type: \"integer\", minimum: 100 },\n },\n required: [\"url\"],\n };\n\n private defaultMaxChars: number;\n\n constructor(params?: { maxChars?: number }) {\n super();\n this.defaultMaxChars = params?.maxChars ?? 50000;\n }\n\n async execute(args: Record<string, unknown>): Promise<string> {\n const url = String(args.url);\n const extractMode = String(args.extractMode ?? \"markdown\");\n const maxChars = args.maxChars\n ? Number(args.maxChars)\n : this.defaultMaxChars;\n\n const { valid, error: validationError } = await validateUrl(url);\n if (!valid) {\n return JSON.stringify({\n error: `URL validation failed: ${validationError}`,\n url,\n });\n }\n\n try {\n let finalResp = await fetch(url, {\n headers: { \"User-Agent\": USER_AGENT },\n redirect: \"manual\",\n signal: AbortSignal.timeout(30000),\n });\n\n let redirectCount = 0;\n const maxRedirects = 5;\n\n while (finalResp.status >= 300 && finalResp.status < 400 && redirectCount < maxRedirects) {\n const location = finalResp.headers.get(\"location\");\n if (!location) break;\n\n const { valid: redirectValid } = await validateUrl(location);\n if (!redirectValid) {\n return JSON.stringify({\n error: `Redirect to blocked address: ${location}`,\n url,\n });\n }\n\n finalResp = await fetch(location, {\n headers: { \"User-Agent\": USER_AGENT },\n redirect: \"manual\",\n signal: AbortSignal.timeout(30000),\n });\n redirectCount++;\n }\n\n if (redirectCount >= maxRedirects) {\n return JSON.stringify({\n error: \"Too many redirects\",\n url,\n });\n }\n\n if (!finalResp.ok) {\n return JSON.stringify({\n error: `HTTP ${finalResp.status}`,\n url,\n });\n }\n\n const contentType = finalResp.headers.get(\"content-type\") ?? \"\";\n const body = await finalResp.text();\n let text: string;\n let extractor: string;\n\n if (contentType.includes(\"application/json\")) {\n try {\n text = JSON.stringify(JSON.parse(body), null, 2);\n } catch {\n text = body;\n }\n extractor = \"json\";\n } else if (\n contentType.includes(\"text/html\") ||\n body.slice(0, 256).toLowerCase().startsWith(\"<!doctype\") ||\n body.slice(0, 256).toLowerCase().startsWith(\"<html\")\n ) {\n // Extract readable content from HTML\n text =\n extractMode === \"markdown\"\n ? htmlToMarkdown(body)\n : stripTags(body);\n extractor = \"html\";\n } else {\n text = body;\n extractor = \"raw\";\n }\n\n const truncated = text.length > maxChars;\n if (truncated) {\n text = text.slice(0, maxChars);\n }\n\n return JSON.stringify({\n url,\n finalUrl: finalResp.url,\n status: finalResp.status,\n extractor,\n truncated,\n length: text.length,\n text,\n });\n } catch (err) {\n return JSON.stringify({\n error: err instanceof Error ? err.message : String(err),\n url,\n });\n }\n }\n}\n"],"mappings":"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"}
|