@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":";;;;cA0Ea,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"}
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"}
@@ -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
- const resp = await fetch(url, {
228
+ let finalResp = await fetch(url, {
148
229
  headers: { "User-Agent": USER_AGENT },
149
- redirect: "follow",
230
+ redirect: "manual",
150
231
  signal: AbortSignal.timeout(3e4)
151
232
  });
152
- if (!resp.ok) return JSON.stringify({
153
- error: `HTTP ${resp.status}`,
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 = resp.headers.get("content-type") ?? "";
157
- const body = await resp.text();
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: resp.url,
179
- status: resp.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(/&amp;/g, \"&\")\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&nbsp;/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(/&amp;/g, \"&\")\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&nbsp;/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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jcheesepkg/nanobot",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
4
4
  "description": "Lightweight AI assistant - TypeScript port",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",