erosolar-cli 1.4.7 → 1.5.1

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.
@@ -0,0 +1,445 @@
1
+ /**
2
+ * Email Tools - SMTP email sending capabilities
3
+ *
4
+ * Supports Gmail, Outlook, and custom SMTP servers with app password authentication.
5
+ * Users configure their email credentials via /secrets or environment variables.
6
+ */
7
+ import * as nodemailer from 'nodemailer';
8
+ // SMTP provider presets
9
+ const SMTP_PRESETS = {
10
+ gmail: {
11
+ host: 'smtp.gmail.com',
12
+ port: 587,
13
+ secure: false, // Uses STARTTLS
14
+ },
15
+ outlook: {
16
+ host: 'smtp.office365.com',
17
+ port: 587,
18
+ secure: false,
19
+ },
20
+ hotmail: {
21
+ host: 'smtp.office365.com',
22
+ port: 587,
23
+ secure: false,
24
+ },
25
+ yahoo: {
26
+ host: 'smtp.mail.yahoo.com',
27
+ port: 587,
28
+ secure: false,
29
+ },
30
+ icloud: {
31
+ host: 'smtp.mail.me.com',
32
+ port: 587,
33
+ secure: false,
34
+ },
35
+ zoho: {
36
+ host: 'smtp.zoho.com',
37
+ port: 587,
38
+ secure: false,
39
+ },
40
+ };
41
+ function getEmailConfig() {
42
+ // Check for provider-specific config first
43
+ const provider = (process.env['SMTP_PROVIDER'] || 'gmail').toLowerCase();
44
+ const user = process.env['SMTP_USER'] || process.env['EMAIL_USER'];
45
+ const password = process.env['SMTP_PASSWORD'] || process.env['EMAIL_APP_PASSWORD'] || process.env['SMTP_APP_PASSWORD'];
46
+ if (!user || !password) {
47
+ return null;
48
+ }
49
+ // Get preset or custom config
50
+ const preset = SMTP_PRESETS[provider];
51
+ return {
52
+ provider,
53
+ host: process.env['SMTP_HOST'] || preset?.host || 'smtp.gmail.com',
54
+ port: parseInt(process.env['SMTP_PORT'] || String(preset?.port || 587), 10),
55
+ secure: process.env['SMTP_SECURE'] === 'true' || preset?.secure || false,
56
+ user,
57
+ password,
58
+ fromName: process.env['SMTP_FROM_NAME'] || process.env['EMAIL_FROM_NAME'],
59
+ };
60
+ }
61
+ async function createTransporter(config) {
62
+ const transporter = nodemailer.createTransport({
63
+ host: config.host,
64
+ port: config.port,
65
+ secure: config.secure,
66
+ auth: {
67
+ user: config.user,
68
+ pass: config.password,
69
+ },
70
+ tls: {
71
+ // Allow self-signed certificates for custom SMTP servers
72
+ rejectUnauthorized: process.env['SMTP_REJECT_UNAUTHORIZED'] !== 'false',
73
+ },
74
+ });
75
+ // Verify connection
76
+ await transporter.verify();
77
+ return transporter;
78
+ }
79
+ export function createEmailTools() {
80
+ return [
81
+ {
82
+ name: 'send_email',
83
+ description: `Send an email via SMTP. Supports Gmail, Outlook, Yahoo, iCloud, Zoho, and custom SMTP servers.
84
+
85
+ Configuration (set via /secrets or environment variables):
86
+ - SMTP_USER or EMAIL_USER: Your email address
87
+ - SMTP_PASSWORD or EMAIL_APP_PASSWORD: Your app password (NOT your regular password)
88
+ - SMTP_PROVIDER: gmail, outlook, yahoo, icloud, zoho (default: gmail)
89
+ - SMTP_FROM_NAME: Display name for sent emails (optional)
90
+
91
+ For Gmail:
92
+ 1. Enable 2-Factor Authentication
93
+ 2. Go to https://myaccount.google.com/apppasswords
94
+ 3. Generate an app password for "Mail"
95
+ 4. Use that app password as SMTP_PASSWORD
96
+
97
+ For Outlook/Hotmail:
98
+ 1. Go to https://account.live.com/proofs/AppPassword
99
+ 2. Generate an app password
100
+ 3. Use that as SMTP_PASSWORD
101
+
102
+ Use cases:
103
+ - Send notifications, reports, or alerts
104
+ - Automated outreach (investors, leads, contacts)
105
+ - Send formatted HTML emails with attachments
106
+ - Batch email campaigns with personalization`,
107
+ parameters: {
108
+ type: 'object',
109
+ properties: {
110
+ to: {
111
+ type: 'string',
112
+ description: 'Recipient email address(es). For multiple recipients, use comma-separated values.',
113
+ },
114
+ subject: {
115
+ type: 'string',
116
+ description: 'Email subject line',
117
+ },
118
+ body: {
119
+ type: 'string',
120
+ description: 'Email body content. Can be plain text or HTML.',
121
+ },
122
+ html: {
123
+ type: 'boolean',
124
+ description: 'Set to true if body contains HTML content (default: false)',
125
+ },
126
+ cc: {
127
+ type: 'string',
128
+ description: 'CC recipient(s), comma-separated (optional)',
129
+ },
130
+ bcc: {
131
+ type: 'string',
132
+ description: 'BCC recipient(s), comma-separated (optional)',
133
+ },
134
+ reply_to: {
135
+ type: 'string',
136
+ description: 'Reply-To address (optional)',
137
+ },
138
+ attachments: {
139
+ type: 'array',
140
+ items: {
141
+ type: 'object',
142
+ properties: {
143
+ filename: { type: 'string' },
144
+ path: { type: 'string', description: 'File path to attach' },
145
+ content: { type: 'string', description: 'Base64 encoded content (alternative to path)' },
146
+ },
147
+ },
148
+ description: 'Array of attachments (optional)',
149
+ },
150
+ },
151
+ required: ['to', 'subject', 'body'],
152
+ },
153
+ handler: async (args) => {
154
+ const config = getEmailConfig();
155
+ if (!config) {
156
+ return `Email not configured. Please set up SMTP credentials:
157
+
158
+ Required environment variables:
159
+ - SMTP_USER or EMAIL_USER: Your email address
160
+ - SMTP_PASSWORD or EMAIL_APP_PASSWORD: Your app password
161
+
162
+ Optional:
163
+ - SMTP_PROVIDER: gmail, outlook, yahoo, icloud, zoho (default: gmail)
164
+ - SMTP_FROM_NAME: Display name for sent emails
165
+
166
+ For Gmail, create an app password at: https://myaccount.google.com/apppasswords
167
+ For Outlook, create an app password at: https://account.live.com/proofs/AppPassword
168
+
169
+ Use /secrets to configure these values.`;
170
+ }
171
+ const to = args['to'];
172
+ const subject = args['subject'];
173
+ const body = args['body'];
174
+ const isHtml = args['html'] === true;
175
+ const cc = args['cc'];
176
+ const bcc = args['bcc'];
177
+ const replyTo = args['reply_to'];
178
+ const attachments = args['attachments'];
179
+ if (!to || !subject || !body) {
180
+ return 'Error: to, subject, and body are required parameters.';
181
+ }
182
+ try {
183
+ const transporter = await createTransporter(config);
184
+ const fromAddress = config.fromName
185
+ ? `"${config.fromName}" <${config.user}>`
186
+ : config.user;
187
+ const mailOptions = {
188
+ from: fromAddress,
189
+ to,
190
+ subject,
191
+ ...(isHtml ? { html: body } : { text: body }),
192
+ ...(cc && { cc }),
193
+ ...(bcc && { bcc }),
194
+ ...(replyTo && { replyTo }),
195
+ ...(attachments && {
196
+ attachments: attachments.map(att => ({
197
+ filename: att.filename,
198
+ ...(att.path ? { path: att.path } : {}),
199
+ ...(att.content ? { content: att.content, encoding: 'base64' } : {}),
200
+ })),
201
+ }),
202
+ };
203
+ const info = await transporter.sendMail(mailOptions);
204
+ return `✅ Email sent successfully!
205
+
206
+ To: ${to}
207
+ Subject: ${subject}
208
+ Message ID: ${info.messageId}
209
+ Provider: ${config.provider}
210
+ ${cc ? `CC: ${cc}` : ''}
211
+ ${bcc ? `BCC: ${bcc}` : ''}
212
+ ${attachments?.length ? `Attachments: ${attachments.length} file(s)` : ''}
213
+
214
+ Response: ${info.response || 'Accepted'}`;
215
+ }
216
+ catch (error) {
217
+ const errorMessage = error instanceof Error ? error.message : String(error);
218
+ // Provide helpful error messages
219
+ if (errorMessage.includes('Invalid login') || errorMessage.includes('authentication')) {
220
+ return `❌ Authentication failed.
221
+
222
+ Error: ${errorMessage}
223
+
224
+ Common fixes:
225
+ 1. Make sure you're using an APP PASSWORD, not your regular password
226
+ 2. For Gmail: https://myaccount.google.com/apppasswords
227
+ 3. For Outlook: https://account.live.com/proofs/AppPassword
228
+ 4. Ensure 2-Factor Authentication is enabled on your account
229
+ 5. Check that SMTP_USER matches the email account`;
230
+ }
231
+ if (errorMessage.includes('ENOTFOUND') || errorMessage.includes('ECONNREFUSED')) {
232
+ return `❌ Could not connect to SMTP server.
233
+
234
+ Error: ${errorMessage}
235
+
236
+ Current config:
237
+ - Host: ${config.host}
238
+ - Port: ${config.port}
239
+ - Provider: ${config.provider}
240
+
241
+ Try setting SMTP_HOST and SMTP_PORT manually if using a custom server.`;
242
+ }
243
+ return `❌ Failed to send email: ${errorMessage}`;
244
+ }
245
+ },
246
+ },
247
+ {
248
+ name: 'verify_email_config',
249
+ description: 'Verify that email/SMTP configuration is correct and can connect to the mail server.',
250
+ parameters: {
251
+ type: 'object',
252
+ properties: {},
253
+ },
254
+ handler: async () => {
255
+ const config = getEmailConfig();
256
+ if (!config) {
257
+ return `❌ Email not configured.
258
+
259
+ Required environment variables:
260
+ - SMTP_USER or EMAIL_USER
261
+ - SMTP_PASSWORD or EMAIL_APP_PASSWORD
262
+
263
+ Optional:
264
+ - SMTP_PROVIDER: gmail, outlook, yahoo, icloud, zoho
265
+ - SMTP_HOST, SMTP_PORT: For custom SMTP servers
266
+ - SMTP_FROM_NAME: Display name
267
+
268
+ Use /secrets to configure these values.`;
269
+ }
270
+ try {
271
+ await createTransporter(config);
272
+ return `✅ Email configuration verified!
273
+
274
+ Provider: ${config.provider}
275
+ Host: ${config.host}
276
+ Port: ${config.port}
277
+ User: ${config.user}
278
+ Secure: ${config.secure ? 'Yes (SSL/TLS)' : 'No (STARTTLS)'}
279
+ From Name: ${config.fromName || '(not set)'}
280
+
281
+ SMTP connection successful. You can now use send_email to send emails.`;
282
+ }
283
+ catch (error) {
284
+ const errorMessage = error instanceof Error ? error.message : String(error);
285
+ return `❌ Email configuration verification failed.
286
+
287
+ Provider: ${config.provider}
288
+ Host: ${config.host}
289
+ Port: ${config.port}
290
+ User: ${config.user}
291
+
292
+ Error: ${errorMessage}
293
+
294
+ Common issues:
295
+ 1. Wrong app password (must use app password, not regular password)
296
+ 2. 2FA not enabled on email account
297
+ 3. Wrong SMTP provider selected
298
+ 4. Network/firewall blocking SMTP ports`;
299
+ }
300
+ },
301
+ },
302
+ {
303
+ name: 'send_batch_emails',
304
+ description: `Send personalized emails to multiple recipients in batch.
305
+
306
+ Useful for:
307
+ - Investor outreach campaigns
308
+ - Lead nurturing sequences
309
+ - Newsletter distribution
310
+ - Bulk notifications with personalization
311
+
312
+ Supports template variables: {{name}}, {{company}}, {{custom_field}}`,
313
+ parameters: {
314
+ type: 'object',
315
+ properties: {
316
+ recipients: {
317
+ type: 'array',
318
+ items: {
319
+ type: 'object',
320
+ properties: {
321
+ email: { type: 'string', description: 'Recipient email' },
322
+ name: { type: 'string', description: 'Recipient name for personalization' },
323
+ company: { type: 'string', description: 'Company name (optional)' },
324
+ custom: {
325
+ type: 'object',
326
+ description: 'Custom fields for template substitution',
327
+ },
328
+ },
329
+ required: ['email'],
330
+ },
331
+ description: 'Array of recipients with personalization data',
332
+ },
333
+ subject_template: {
334
+ type: 'string',
335
+ description: 'Subject line template with {{variables}}',
336
+ },
337
+ body_template: {
338
+ type: 'string',
339
+ description: 'Email body template with {{variables}}',
340
+ },
341
+ html: {
342
+ type: 'boolean',
343
+ description: 'Set to true if body contains HTML (default: false)',
344
+ },
345
+ delay_ms: {
346
+ type: 'number',
347
+ description: 'Delay between emails in milliseconds (default: 1000). Helps avoid rate limits.',
348
+ },
349
+ },
350
+ required: ['recipients', 'subject_template', 'body_template'],
351
+ },
352
+ handler: async (args) => {
353
+ const config = getEmailConfig();
354
+ if (!config) {
355
+ return 'Email not configured. Use /secrets to set SMTP_USER and SMTP_PASSWORD.';
356
+ }
357
+ const recipients = args['recipients'];
358
+ const subjectTemplate = args['subject_template'];
359
+ const bodyTemplate = args['body_template'];
360
+ const isHtml = args['html'] === true;
361
+ const delayMs = args['delay_ms'] || 1000;
362
+ if (!recipients?.length || !subjectTemplate || !bodyTemplate) {
363
+ return 'Error: recipients, subject_template, and body_template are required.';
364
+ }
365
+ try {
366
+ const transporter = await createTransporter(config);
367
+ const fromAddress = config.fromName
368
+ ? `"${config.fromName}" <${config.user}>`
369
+ : config.user;
370
+ const results = [];
371
+ for (let i = 0; i < recipients.length; i++) {
372
+ const recipient = recipients[i];
373
+ if (!recipient)
374
+ continue;
375
+ // Apply template substitution
376
+ const personalizedSubject = applyTemplate(subjectTemplate, recipient);
377
+ const personalizedBody = applyTemplate(bodyTemplate, recipient);
378
+ try {
379
+ await transporter.sendMail({
380
+ from: fromAddress,
381
+ to: recipient.email,
382
+ subject: personalizedSubject,
383
+ ...(isHtml ? { html: personalizedBody } : { text: personalizedBody }),
384
+ });
385
+ results.push({ email: recipient.email, success: true });
386
+ }
387
+ catch (error) {
388
+ results.push({
389
+ email: recipient.email,
390
+ success: false,
391
+ error: error instanceof Error ? error.message : String(error),
392
+ });
393
+ }
394
+ // Delay between emails (except for last one)
395
+ if (i < recipients.length - 1 && delayMs > 0) {
396
+ await new Promise(resolve => setTimeout(resolve, delayMs));
397
+ }
398
+ }
399
+ const successful = results.filter(r => r.success).length;
400
+ const failed = results.filter(r => !r.success);
401
+ let summary = `📧 Batch Email Results
402
+
403
+ Total: ${recipients.length}
404
+ Sent: ${successful}
405
+ Failed: ${failed.length}
406
+ Provider: ${config.provider}
407
+ `;
408
+ if (failed.length > 0) {
409
+ summary += '\n❌ Failed emails:\n';
410
+ for (const f of failed) {
411
+ summary += `- ${f.email}: ${f.error}\n`;
412
+ }
413
+ }
414
+ return summary;
415
+ }
416
+ catch (error) {
417
+ return `❌ Batch email failed: ${error instanceof Error ? error.message : String(error)}`;
418
+ }
419
+ },
420
+ },
421
+ ];
422
+ }
423
+ function applyTemplate(template, data) {
424
+ let result = template;
425
+ // Standard fields
426
+ result = result.replace(/\{\{email\}\}/gi, data.email);
427
+ result = result.replace(/\{\{name\}\}/gi, data.name || '');
428
+ result = result.replace(/\{\{company\}\}/gi, data.company || '');
429
+ // First name extraction
430
+ const firstName = data.name?.split(' ')[0] || '';
431
+ result = result.replace(/\{\{first_name\}\}/gi, firstName);
432
+ result = result.replace(/\{\{firstName\}\}/gi, firstName);
433
+ // Custom fields
434
+ if (data.custom) {
435
+ for (const [key, value] of Object.entries(data.custom)) {
436
+ const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'gi');
437
+ result = result.replace(regex, value);
438
+ }
439
+ }
440
+ // Clean up any remaining unmatched placeholders
441
+ result = result.replace(/\{\{[^}]+\}\}/g, '');
442
+ return result;
443
+ }
444
+ export { getEmailConfig };
445
+ //# sourceMappingURL=emailTools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emailTools.js","sourceRoot":"","sources":["../../src/tools/emailTools.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"webTools.d.ts","sourceRoot":"","sources":["../../src/tools/webTools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAI7D,wBAAgB,cAAc,IAAI,cAAc,EAAE,CA0IjD"}
1
+ {"version":3,"file":"webTools.d.ts","sourceRoot":"","sources":["../../src/tools/webTools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAI7D,wBAAgB,cAAc,IAAI,cAAc,EAAE,CAgPjD"}
@@ -12,7 +12,7 @@ export function createWebTools() {
12
12
  - Use this tool when you need to retrieve and analyze web content
13
13
 
14
14
  Usage notes:
15
- - IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions
15
+ - IMPORTANT: If TAVILY_API_KEY is set, use WebExtract instead for better content extraction
16
16
  - The URL must be a fully-formed valid URL
17
17
  - HTTP URLs will be automatically upgraded to HTTPS
18
18
  - The prompt should describe what information you want to extract from the page
@@ -62,6 +62,95 @@ Summary: This is the content fetched from the URL. In a full implementation, thi
62
62
  }
63
63
  },
64
64
  },
65
+ {
66
+ name: 'WebExtract',
67
+ description: `- Extracts clean, structured content from one or more URLs using Tavily Extract API
68
+ - Superior to WebFetch for content extraction when TAVILY_API_KEY is available
69
+ - Returns raw text content optimized for LLM consumption
70
+ - Supports batch extraction of up to 20 URLs in a single call
71
+
72
+ Usage notes:
73
+ - Requires TAVILY_API_KEY environment variable
74
+ - Best for extracting article content, documentation, blog posts
75
+ - Returns clean text without HTML artifacts
76
+ - More reliable than basic HTML parsing for complex pages
77
+ - Use for deep content extraction when you need full page text`,
78
+ parameters: {
79
+ type: 'object',
80
+ properties: {
81
+ urls: {
82
+ type: 'array',
83
+ items: { type: 'string' },
84
+ description: 'Array of URLs to extract content from (max 20)',
85
+ },
86
+ },
87
+ required: ['urls'],
88
+ },
89
+ handler: async (args) => {
90
+ const urls = args['urls'];
91
+ if (!urls || !Array.isArray(urls) || urls.length === 0) {
92
+ return 'Error: urls parameter is required and must be a non-empty array.';
93
+ }
94
+ if (urls.length > 20) {
95
+ return 'Error: Maximum 20 URLs allowed per request.';
96
+ }
97
+ const tavilyKey = process.env['TAVILY_API_KEY']?.trim();
98
+ if (!tavilyKey) {
99
+ return [
100
+ 'WebExtract requires TAVILY_API_KEY to be set.',
101
+ 'Get your API key at: https://tavily.com',
102
+ '',
103
+ 'Falling back to WebFetch for basic extraction is available as an alternative.',
104
+ ].join('\n');
105
+ }
106
+ try {
107
+ const response = await fetch('https://api.tavily.com/extract', {
108
+ method: 'POST',
109
+ headers: {
110
+ 'Content-Type': 'application/json',
111
+ },
112
+ body: JSON.stringify({
113
+ api_key: tavilyKey,
114
+ urls: urls,
115
+ }),
116
+ });
117
+ if (!response.ok) {
118
+ const errorText = await response.text().catch(() => '');
119
+ throw new Error(`Tavily Extract returned HTTP ${response.status}: ${errorText}`);
120
+ }
121
+ const payload = (await response.json());
122
+ const results = payload.results || [];
123
+ const failedUrls = payload.failed_results || [];
124
+ if (results.length === 0 && failedUrls.length === urls.length) {
125
+ return `Failed to extract content from all ${urls.length} URLs. This may be due to access restrictions or invalid URLs.`;
126
+ }
127
+ let output = `Extracted content from ${results.length}/${urls.length} URLs:\n\n`;
128
+ for (const result of results) {
129
+ output += `--- ${result.url} ---\n`;
130
+ if (result.raw_content) {
131
+ // Truncate very long content
132
+ const content = result.raw_content.length > 10000
133
+ ? result.raw_content.slice(0, 10000) + '\n\n... (content truncated)'
134
+ : result.raw_content;
135
+ output += `${content}\n\n`;
136
+ }
137
+ else {
138
+ output += '(No content extracted)\n\n';
139
+ }
140
+ }
141
+ if (failedUrls.length > 0) {
142
+ output += `\nFailed URLs (${failedUrls.length}):\n`;
143
+ for (const failed of failedUrls) {
144
+ output += `- ${failed.url}: ${failed.error || 'Unknown error'}\n`;
145
+ }
146
+ }
147
+ return output.trim();
148
+ }
149
+ catch (error) {
150
+ return `Error extracting content: ${error instanceof Error ? error.message : String(error)}`;
151
+ }
152
+ },
153
+ },
65
154
  {
66
155
  name: 'WebSearch',
67
156
  description: `- Allows Claude to search the web and use the results to inform responses
@@ -109,8 +198,12 @@ Usage notes:
109
198
  const provider = resolveSearchProvider();
110
199
  if (!provider) {
111
200
  return [
112
- 'WebSearch requires either BRAVE_SEARCH_API_KEY or SERPAPI_API_KEY.',
201
+ 'WebSearch requires TAVILY_API_KEY (recommended), BRAVE_SEARCH_API_KEY, or SERPAPI_API_KEY.',
113
202
  'Run /secrets (or set the environment variables directly) to configure an API key.',
203
+ '',
204
+ 'Get your Tavily API key at: https://tavily.com (recommended)',
205
+ 'Get your Brave Search API key at: https://brave.com/search/api/',
206
+ 'Get your SerpAPI key at: https://serpapi.com/',
114
207
  ].join('\n');
115
208
  }
116
209
  const results = await provider.search({
@@ -132,6 +225,15 @@ Usage notes:
132
225
  ];
133
226
  }
134
227
  function resolveSearchProvider() {
228
+ // Tavily is the preferred search provider
229
+ const tavilyKey = process.env['TAVILY_API_KEY']?.trim();
230
+ if (tavilyKey) {
231
+ return {
232
+ id: 'tavily',
233
+ label: 'Tavily Search',
234
+ search: (params) => performTavilySearch(params, tavilyKey),
235
+ };
236
+ }
135
237
  const braveKey = process.env['BRAVE_SEARCH_API_KEY']?.trim();
136
238
  if (braveKey) {
137
239
  return {
@@ -199,6 +301,42 @@ async function performSerpApiSearch(params, apiKey) {
199
301
  .filter((result) => Boolean(result.url));
200
302
  return applyDomainFilters(mapped, params.allowedDomains, params.blockedDomains).slice(0, params.maxResults);
201
303
  }
304
+ async function performTavilySearch(params, apiKey) {
305
+ const response = await fetch('https://api.tavily.com/search', {
306
+ method: 'POST',
307
+ headers: {
308
+ 'Content-Type': 'application/json',
309
+ },
310
+ body: JSON.stringify({
311
+ api_key: apiKey,
312
+ query: params.query,
313
+ search_depth: 'advanced',
314
+ include_answer: true,
315
+ include_raw_content: false,
316
+ max_results: Math.min(params.maxResults * 2, 10),
317
+ include_domains: params.allowedDomains.length ? params.allowedDomains : undefined,
318
+ exclude_domains: params.blockedDomains.length ? params.blockedDomains : undefined,
319
+ }),
320
+ });
321
+ if (!response.ok) {
322
+ const errorText = await response.text().catch(() => '');
323
+ throw new Error(`Tavily Search returned HTTP ${response.status}: ${errorText}`);
324
+ }
325
+ const payload = (await response.json());
326
+ const entries = Array.isArray(payload?.results) ? payload.results : [];
327
+ const mapped = entries
328
+ .map((entry) => ({
329
+ title: entry.title || entry.url,
330
+ url: entry.url,
331
+ snippet: entry.content || '',
332
+ source: safeHostname(entry.url) || undefined,
333
+ published: entry.published_date,
334
+ score: entry.score,
335
+ }))
336
+ .filter((result) => Boolean(result.url));
337
+ // Tavily already handles domain filtering via include_domains/exclude_domains
338
+ return mapped.slice(0, params.maxResults);
339
+ }
202
340
  function parseDomainList(value) {
203
341
  if (!Array.isArray(value)) {
204
342
  return [];