nodebench-mcp 2.15.0 → 2.18.0

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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/NODEBENCH_AGENTS.md +2 -2
  3. package/README.md +514 -82
  4. package/dist/__tests__/analytics.test.d.ts +11 -0
  5. package/dist/__tests__/analytics.test.js +546 -0
  6. package/dist/__tests__/analytics.test.js.map +1 -0
  7. package/dist/__tests__/architectComplex.test.d.ts +1 -0
  8. package/dist/__tests__/architectComplex.test.js +375 -0
  9. package/dist/__tests__/architectComplex.test.js.map +1 -0
  10. package/dist/__tests__/architectSmoke.test.d.ts +1 -0
  11. package/dist/__tests__/architectSmoke.test.js +92 -0
  12. package/dist/__tests__/architectSmoke.test.js.map +1 -0
  13. package/dist/__tests__/dynamicLoading.test.d.ts +1 -0
  14. package/dist/__tests__/dynamicLoading.test.js +278 -0
  15. package/dist/__tests__/dynamicLoading.test.js.map +1 -0
  16. package/dist/__tests__/evalHarness.test.js +7 -2
  17. package/dist/__tests__/evalHarness.test.js.map +1 -1
  18. package/dist/__tests__/gaiaCapabilityEval.test.js +229 -12
  19. package/dist/__tests__/gaiaCapabilityEval.test.js.map +1 -1
  20. package/dist/__tests__/gaiaCapabilityMediaEval.test.js +194 -109
  21. package/dist/__tests__/gaiaCapabilityMediaEval.test.js.map +1 -1
  22. package/dist/__tests__/helpers/answerMatch.js +22 -22
  23. package/dist/__tests__/presetRealWorldBench.test.js +11 -2
  24. package/dist/__tests__/presetRealWorldBench.test.js.map +1 -1
  25. package/dist/__tests__/tools.test.js +10 -4
  26. package/dist/__tests__/tools.test.js.map +1 -1
  27. package/dist/__tests__/toolsetGatingEval.test.js +12 -4
  28. package/dist/__tests__/toolsetGatingEval.test.js.map +1 -1
  29. package/dist/analytics/index.d.ts +10 -0
  30. package/dist/analytics/index.js +11 -0
  31. package/dist/analytics/index.js.map +1 -0
  32. package/dist/analytics/projectDetector.d.ts +19 -0
  33. package/dist/analytics/projectDetector.js +259 -0
  34. package/dist/analytics/projectDetector.js.map +1 -0
  35. package/dist/analytics/schema.d.ts +57 -0
  36. package/dist/analytics/schema.js +157 -0
  37. package/dist/analytics/schema.js.map +1 -0
  38. package/dist/analytics/smartPreset.d.ts +63 -0
  39. package/dist/analytics/smartPreset.js +300 -0
  40. package/dist/analytics/smartPreset.js.map +1 -0
  41. package/dist/analytics/toolTracker.d.ts +59 -0
  42. package/dist/analytics/toolTracker.js +163 -0
  43. package/dist/analytics/toolTracker.js.map +1 -0
  44. package/dist/analytics/usageStats.d.ts +64 -0
  45. package/dist/analytics/usageStats.js +252 -0
  46. package/dist/analytics/usageStats.js.map +1 -0
  47. package/dist/db.js +359 -321
  48. package/dist/db.js.map +1 -1
  49. package/dist/index.d.ts +2 -1
  50. package/dist/index.js +653 -84
  51. package/dist/index.js.map +1 -1
  52. package/dist/tools/architectTools.d.ts +15 -0
  53. package/dist/tools/architectTools.js +304 -0
  54. package/dist/tools/architectTools.js.map +1 -0
  55. package/dist/tools/critterTools.js +14 -14
  56. package/dist/tools/emailTools.d.ts +15 -0
  57. package/dist/tools/emailTools.js +664 -0
  58. package/dist/tools/emailTools.js.map +1 -0
  59. package/dist/tools/metaTools.js +660 -0
  60. package/dist/tools/metaTools.js.map +1 -1
  61. package/dist/tools/parallelAgentTools.js +176 -176
  62. package/dist/tools/patternTools.js +11 -11
  63. package/dist/tools/progressiveDiscoveryTools.d.ts +5 -1
  64. package/dist/tools/progressiveDiscoveryTools.js +113 -21
  65. package/dist/tools/progressiveDiscoveryTools.js.map +1 -1
  66. package/dist/tools/researchWritingTools.js +42 -42
  67. package/dist/tools/rssTools.d.ts +8 -0
  68. package/dist/tools/rssTools.js +833 -0
  69. package/dist/tools/rssTools.js.map +1 -0
  70. package/dist/tools/toolRegistry.d.ts +17 -0
  71. package/dist/tools/toolRegistry.js +236 -17
  72. package/dist/tools/toolRegistry.js.map +1 -1
  73. package/dist/tools/voiceBridgeTools.js +498 -498
  74. package/dist/toolsetRegistry.d.ts +10 -0
  75. package/dist/toolsetRegistry.js +84 -0
  76. package/dist/toolsetRegistry.js.map +1 -0
  77. package/package.json +12 -5
@@ -0,0 +1,664 @@
1
+ /**
2
+ * Email Tools — Send and read emails via raw SMTP/IMAP over TLS.
3
+ *
4
+ * Zero npm dependencies — uses Node's built-in `tls` module.
5
+ *
6
+ * Environment variables:
7
+ * - EMAIL_USER: Email address (e.g., agent@gmail.com)
8
+ * - EMAIL_PASS: App password (NOT regular password for Gmail)
9
+ * - EMAIL_SMTP_HOST: SMTP server (default: smtp.gmail.com)
10
+ * - EMAIL_SMTP_PORT: SMTP port (default: 465)
11
+ * - EMAIL_IMAP_HOST: IMAP server (default: imap.gmail.com)
12
+ * - EMAIL_IMAP_PORT: IMAP port (default: 993)
13
+ */
14
+ import * as tls from "node:tls";
15
+ // ── Config ───────────────────────────────────────────────────────────────────
16
+ function getSmtpConfig() {
17
+ const user = process.env.EMAIL_USER;
18
+ const pass = process.env.EMAIL_PASS;
19
+ if (!user || !pass)
20
+ throw new Error("EMAIL_USER and EMAIL_PASS environment variables required. For Gmail, use an App Password (Google Account → Security → 2-Step Verification → App passwords).");
21
+ return {
22
+ host: process.env.EMAIL_SMTP_HOST || "smtp.gmail.com",
23
+ port: parseInt(process.env.EMAIL_SMTP_PORT || "465"),
24
+ user,
25
+ pass,
26
+ };
27
+ }
28
+ function getImapConfig() {
29
+ const user = process.env.EMAIL_USER;
30
+ const pass = process.env.EMAIL_PASS;
31
+ if (!user || !pass)
32
+ throw new Error("EMAIL_USER and EMAIL_PASS environment variables required. For Gmail, enable IMAP in Settings → Forwarding and POP/IMAP.");
33
+ return {
34
+ host: process.env.EMAIL_IMAP_HOST || "imap.gmail.com",
35
+ port: parseInt(process.env.EMAIL_IMAP_PORT || "993"),
36
+ user,
37
+ pass,
38
+ };
39
+ }
40
+ // ── SMTP helpers ─────────────────────────────────────────────────────────────
41
+ /** Read complete SMTP response (handles multi-line 250-... continuations) */
42
+ function readSmtp(socket, timeoutMs = 10000) {
43
+ return new Promise((resolve, reject) => {
44
+ let buf = "";
45
+ const timer = setTimeout(() => {
46
+ socket.removeAllListeners("data");
47
+ reject(new Error("SMTP read timeout"));
48
+ }, timeoutMs);
49
+ const onData = (chunk) => {
50
+ buf += chunk.toString();
51
+ // Complete when a line has "NNN " (space after 3-digit code, not dash)
52
+ const lines = buf.split("\r\n").filter(Boolean);
53
+ const last = lines[lines.length - 1];
54
+ if (last && /^\d{3} /.test(last)) {
55
+ clearTimeout(timer);
56
+ socket.removeListener("data", onData);
57
+ const code = parseInt(last.substring(0, 3));
58
+ if (code >= 400)
59
+ reject(new Error(`SMTP ${code}: ${buf.trim()}`));
60
+ else
61
+ resolve(buf.trim());
62
+ }
63
+ };
64
+ socket.on("data", onData);
65
+ });
66
+ }
67
+ /** Send SMTP command and wait for response */
68
+ async function smtpCmd(socket, cmd) {
69
+ socket.write(cmd + "\r\n");
70
+ return readSmtp(socket);
71
+ }
72
+ /** Build RFC 2822 MIME message */
73
+ function buildMessage(opts) {
74
+ const boundary = `----NodeBench${Date.now()}`;
75
+ const headers = [
76
+ `From: ${opts.from}`,
77
+ `To: ${opts.to.join(", ")}`,
78
+ ...(opts.cc?.length ? [`Cc: ${opts.cc.join(", ")}`] : []),
79
+ `Subject: ${opts.subject}`,
80
+ `Date: ${new Date().toUTCString()}`,
81
+ `MIME-Version: 1.0`,
82
+ `X-Mailer: NodeBench-MCP`,
83
+ ];
84
+ if (opts.html) {
85
+ headers.push(`Content-Type: multipart/alternative; boundary="${boundary}"`);
86
+ return [
87
+ ...headers,
88
+ "",
89
+ `--${boundary}`,
90
+ "Content-Type: text/plain; charset=UTF-8",
91
+ "",
92
+ opts.body,
93
+ `--${boundary}`,
94
+ "Content-Type: text/html; charset=UTF-8",
95
+ "",
96
+ opts.html,
97
+ `--${boundary}--`,
98
+ ].join("\r\n");
99
+ }
100
+ headers.push("Content-Type: text/plain; charset=UTF-8");
101
+ return [...headers, "", opts.body].join("\r\n");
102
+ }
103
+ /** Send email via SMTP over TLS (port 465) */
104
+ async function sendEmail(opts) {
105
+ const config = getSmtpConfig();
106
+ const socket = tls.connect({ host: config.host, port: config.port, rejectUnauthorized: true });
107
+ await new Promise((resolve, reject) => {
108
+ socket.once("secureConnect", resolve);
109
+ socket.once("error", reject);
110
+ });
111
+ try {
112
+ await readSmtp(socket); // 220 greeting
113
+ await smtpCmd(socket, `EHLO nodebench`);
114
+ await smtpCmd(socket, `AUTH LOGIN`);
115
+ await smtpCmd(socket, Buffer.from(config.user).toString("base64"));
116
+ await smtpCmd(socket, Buffer.from(config.pass).toString("base64"));
117
+ await smtpCmd(socket, `MAIL FROM:<${config.user}>`);
118
+ const allRecipients = [...opts.to, ...(opts.cc || []), ...(opts.bcc || [])];
119
+ for (const rcpt of allRecipients) {
120
+ await smtpCmd(socket, `RCPT TO:<${rcpt}>`);
121
+ }
122
+ await smtpCmd(socket, "DATA");
123
+ const message = buildMessage({
124
+ from: config.user,
125
+ to: opts.to,
126
+ cc: opts.cc,
127
+ subject: opts.subject,
128
+ body: opts.body,
129
+ html: opts.html,
130
+ });
131
+ // Dot-stuff: lines starting with "." get an extra "." per RFC 5321
132
+ const escaped = message.replace(/\r\n\./g, "\r\n..");
133
+ socket.write(escaped + "\r\n.\r\n");
134
+ await readSmtp(socket); // 250 OK
135
+ await smtpCmd(socket, "QUIT").catch(() => { }); // QUIT may not get a reply
136
+ return `Email sent to ${opts.to.join(", ")}`;
137
+ }
138
+ finally {
139
+ socket.destroy();
140
+ }
141
+ }
142
+ /** Read IMAP response until tagged completion (e.g., "A001 OK ...") */
143
+ function readImap(socket, tag, timeoutMs = 15000) {
144
+ return new Promise((resolve, reject) => {
145
+ let buf = "";
146
+ const timer = setTimeout(() => {
147
+ socket.removeAllListeners("data");
148
+ reject(new Error(`IMAP read timeout waiting for ${tag}`));
149
+ }, timeoutMs);
150
+ const onData = (chunk) => {
151
+ buf += chunk.toString();
152
+ if (buf.includes(`${tag} OK`) || buf.includes(`${tag} NO`) || buf.includes(`${tag} BAD`)) {
153
+ clearTimeout(timer);
154
+ socket.removeListener("data", onData);
155
+ if (buf.includes(`${tag} NO`) || buf.includes(`${tag} BAD`)) {
156
+ reject(new Error(`IMAP error: ${buf.substring(buf.indexOf(tag), buf.indexOf(tag) + 200).trim()}`));
157
+ }
158
+ else {
159
+ resolve(buf.trim());
160
+ }
161
+ }
162
+ };
163
+ socket.on("data", onData);
164
+ });
165
+ }
166
+ /** Read untagged IMAP greeting */
167
+ function readImapGreeting(socket) {
168
+ return new Promise((resolve, reject) => {
169
+ const timer = setTimeout(() => reject(new Error("IMAP greeting timeout")), 10000);
170
+ const onData = (chunk) => {
171
+ const data = chunk.toString();
172
+ if (data.startsWith("* OK") || data.startsWith("* PREAUTH")) {
173
+ clearTimeout(timer);
174
+ socket.removeListener("data", onData);
175
+ resolve(data.trim());
176
+ }
177
+ };
178
+ socket.on("data", onData);
179
+ });
180
+ }
181
+ /** Extract header value from raw RFC822 headers */
182
+ function getHeader(headers, name) {
183
+ // Handle folded headers (continuation lines start with whitespace)
184
+ const re = new RegExp(`^${name}:\\s*(.+?)(?=\\r\\n[^\\s]|\\r\\n$|$)`, "ims");
185
+ const m = headers.match(re);
186
+ return m ? m[1].replace(/\r\n\s+/g, " ").trim() : "";
187
+ }
188
+ /** Parse basic email from raw RFC822 text */
189
+ function parseRawEmail(raw, msgId) {
190
+ // Split headers from body at first double CRLF
191
+ const splitIdx = raw.indexOf("\r\n\r\n");
192
+ const headersPart = splitIdx > 0 ? raw.substring(0, splitIdx) : raw;
193
+ const bodyPart = splitIdx > 0 ? raw.substring(splitIdx + 4) : "";
194
+ // For multipart, try to extract text/plain
195
+ let body = bodyPart;
196
+ const ctHeader = getHeader(headersPart, "Content-Type");
197
+ if (ctHeader.includes("multipart")) {
198
+ const bMatch = ctHeader.match(/boundary="?([^";\r\n]+)"?/);
199
+ if (bMatch) {
200
+ const parts = body.split(`--${bMatch[1]}`);
201
+ for (const part of parts) {
202
+ if (part.toLowerCase().includes("text/plain")) {
203
+ const partSplit = part.indexOf("\r\n\r\n");
204
+ if (partSplit > 0) {
205
+ body = part.substring(partSplit + 4).trim();
206
+ break;
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+ // Clean trailing IMAP artifacts
213
+ body = body.replace(/\)\s*$/, "").trim();
214
+ return {
215
+ id: msgId,
216
+ from: getHeader(headersPart, "From"),
217
+ to: getHeader(headersPart, "To"),
218
+ subject: getHeader(headersPart, "Subject"),
219
+ date: getHeader(headersPart, "Date"),
220
+ body: body.substring(0, 5000),
221
+ };
222
+ }
223
+ /** Read emails from IMAP mailbox */
224
+ async function readEmails(opts) {
225
+ const config = getImapConfig();
226
+ const folder = opts.folder || "INBOX";
227
+ const limit = Math.min(opts.limit || 10, 50);
228
+ const search = opts.search || "ALL";
229
+ const socket = tls.connect({ host: config.host, port: config.port, rejectUnauthorized: true });
230
+ await new Promise((resolve, reject) => {
231
+ socket.once("secureConnect", resolve);
232
+ socket.once("error", reject);
233
+ });
234
+ let tagNum = 0;
235
+ const tag = () => `A${String(++tagNum).padStart(4, "0")}`;
236
+ try {
237
+ await readImapGreeting(socket);
238
+ const t1 = tag();
239
+ socket.write(`${t1} LOGIN "${config.user}" "${config.pass}"\r\n`);
240
+ await readImap(socket, t1);
241
+ const t2 = tag();
242
+ socket.write(`${t2} SELECT "${folder}"\r\n`);
243
+ await readImap(socket, t2);
244
+ const t3 = tag();
245
+ socket.write(`${t3} SEARCH ${search}\r\n`);
246
+ const searchResult = await readImap(socket, t3);
247
+ // Parse "* SEARCH 1 2 3 4 5"
248
+ const searchLine = searchResult.split("\r\n").find((l) => l.startsWith("* SEARCH"));
249
+ const ids = searchLine
250
+ ? searchLine.replace("* SEARCH", "").trim().split(/\s+/).filter(Boolean)
251
+ : [];
252
+ const recentIds = ids.slice(-limit);
253
+ const messages = [];
254
+ for (const id of recentIds) {
255
+ const t = tag();
256
+ socket.write(`${t} FETCH ${id} RFC822\r\n`);
257
+ const fetchResult = await readImap(socket, t, 30000);
258
+ messages.push(parseRawEmail(fetchResult, id));
259
+ }
260
+ const tLogout = tag();
261
+ socket.write(`${tLogout} LOGOUT\r\n`);
262
+ await readImap(socket, tLogout).catch(() => { });
263
+ return messages.reverse(); // Most recent first
264
+ }
265
+ finally {
266
+ socket.destroy();
267
+ }
268
+ }
269
+ // ── Tools ────────────────────────────────────────────────────────────────────
270
+ export const emailTools = [
271
+ {
272
+ name: "send_email",
273
+ description: "Send an email via SMTP over TLS. Requires EMAIL_USER and EMAIL_PASS env vars. Defaults to Gmail SMTP (smtp.gmail.com:465). For Gmail, use an App Password (Google Account → Security → 2-Step Verification → App passwords). Override host/port with EMAIL_SMTP_HOST and EMAIL_SMTP_PORT. Supports plain text and HTML multipart.",
274
+ inputSchema: {
275
+ type: "object",
276
+ properties: {
277
+ to: {
278
+ type: "array",
279
+ items: { type: "string" },
280
+ description: "Recipient email addresses",
281
+ },
282
+ subject: {
283
+ type: "string",
284
+ description: "Email subject line",
285
+ },
286
+ body: {
287
+ type: "string",
288
+ description: "Plain text email body",
289
+ },
290
+ cc: {
291
+ type: "array",
292
+ items: { type: "string" },
293
+ description: "CC recipients (optional)",
294
+ },
295
+ bcc: {
296
+ type: "array",
297
+ items: { type: "string" },
298
+ description: "BCC recipients (optional)",
299
+ },
300
+ html: {
301
+ type: "string",
302
+ description: "HTML email body (optional — sent as multipart/alternative with plain text)",
303
+ },
304
+ },
305
+ required: ["to", "subject", "body"],
306
+ },
307
+ handler: async (args) => {
308
+ const to = args.to;
309
+ const subject = args.subject;
310
+ const body = args.body;
311
+ const cc = args.cc;
312
+ const bcc = args.bcc;
313
+ const html = args.html;
314
+ const result = await sendEmail({ to, subject, body, cc, bcc, html });
315
+ return [{ type: "text", text: JSON.stringify({ success: true, message: result }) }];
316
+ },
317
+ },
318
+ {
319
+ name: "read_emails",
320
+ description: "Read emails from an IMAP mailbox over TLS. Requires EMAIL_USER and EMAIL_PASS env vars. Defaults to Gmail IMAP (imap.gmail.com:993). For Gmail, enable IMAP in Settings and use an App Password. Returns emails with headers (from, to, subject, date) and plain text body. Use IMAP SEARCH syntax to filter.",
321
+ inputSchema: {
322
+ type: "object",
323
+ properties: {
324
+ folder: {
325
+ type: "string",
326
+ description: 'IMAP folder (default: INBOX). Common: INBOX, Sent, Drafts, [Gmail]/All Mail',
327
+ },
328
+ limit: {
329
+ type: "number",
330
+ description: "Maximum emails to return, most recent first (default: 10, max: 50)",
331
+ },
332
+ search: {
333
+ type: "string",
334
+ description: 'IMAP SEARCH criteria (default: ALL). Examples: UNSEEN, FROM "sender@example.com", SUBJECT "keyword", SINCE 01-Jan-2026, OR UNSEEN FLAGGED',
335
+ },
336
+ },
337
+ required: [],
338
+ },
339
+ handler: async (args) => {
340
+ const folder = args.folder;
341
+ const limit = args.limit;
342
+ const search = args.search;
343
+ const messages = await readEmails({ folder, limit, search });
344
+ return [
345
+ {
346
+ type: "text",
347
+ text: JSON.stringify({ count: messages.length, emails: messages }),
348
+ },
349
+ ];
350
+ },
351
+ },
352
+ {
353
+ name: "draft_email_reply",
354
+ description: "Structure an email thread for reply drafting. Parses the thread, extracts context (from, subject, date), and builds a reply prompt with your instructions and desired tone. Returns a structured draft ready to review and send via send_email, or to refine via call_llm.",
355
+ inputSchema: {
356
+ type: "object",
357
+ properties: {
358
+ thread: {
359
+ type: "string",
360
+ description: "The original email thread text (copy-paste or from read_emails output)",
361
+ },
362
+ instructions: {
363
+ type: "string",
364
+ description: 'Instructions for the reply (e.g., "Accept the meeting", "Decline politely and suggest next week", "Ask for clarification on the budget")',
365
+ },
366
+ tone: {
367
+ type: "string",
368
+ enum: ["professional", "casual", "formal", "friendly"],
369
+ description: "Desired tone for the reply (default: professional)",
370
+ },
371
+ include_original: {
372
+ type: "boolean",
373
+ description: "Include the original message below the reply (default: true)",
374
+ },
375
+ },
376
+ required: ["thread", "instructions"],
377
+ },
378
+ handler: async (args) => {
379
+ const thread = args.thread;
380
+ const instructions = args.instructions;
381
+ const tone = args.tone || "professional";
382
+ const includeOriginal = args.include_original !== false;
383
+ // Extract context from the thread
384
+ const fromMatch = thread.match(/[Ff]rom:\s*(.+)/);
385
+ const subjectMatch = thread.match(/[Ss]ubject:\s*(.+)/);
386
+ const dateMatch = thread.match(/[Dd]ate:\s*(.+)/);
387
+ const replySubject = subjectMatch
388
+ ? subjectMatch[1].startsWith("Re:") ? subjectMatch[1].trim() : `Re: ${subjectMatch[1].trim()}`
389
+ : "Re: (no subject)";
390
+ const draft = {
391
+ to: fromMatch ? fromMatch[1].trim() : "(extract from thread)",
392
+ subject: replySubject,
393
+ replyPrompt: [
394
+ `Draft a ${tone} email reply.`,
395
+ `Instructions: ${instructions}`,
396
+ "",
397
+ "--- Original thread ---",
398
+ thread.substring(0, 3000),
399
+ "--- End of thread ---",
400
+ ].join("\n"),
401
+ quotedOriginal: includeOriginal
402
+ ? `\n\n--- Original Message ---\n${thread.substring(0, 2000)}`
403
+ : "",
404
+ metadata: {
405
+ originalFrom: fromMatch?.[1]?.trim(),
406
+ originalSubject: subjectMatch?.[1]?.trim(),
407
+ originalDate: dateMatch?.[1]?.trim(),
408
+ tone,
409
+ },
410
+ };
411
+ return [{ type: "text", text: JSON.stringify(draft) }];
412
+ },
413
+ },
414
+ {
415
+ name: "check_email_setup",
416
+ description: "Diagnostic wizard for email tool configuration. Checks env vars (EMAIL_USER, EMAIL_PASS, etc.), optionally tests SMTP/IMAP connections, and returns step-by-step setup instructions for missing pieces. Supports Gmail, Outlook, Yahoo, and custom SMTP/IMAP. Run this FIRST before using send_email or read_emails.",
417
+ inputSchema: {
418
+ type: "object",
419
+ properties: {
420
+ provider: {
421
+ type: "string",
422
+ enum: ["gmail", "outlook", "yahoo", "custom"],
423
+ description: "Email provider for tailored setup instructions (default: auto-detect from EMAIL_USER or 'gmail')",
424
+ },
425
+ test_connection: {
426
+ type: "boolean",
427
+ description: "Actually test SMTP and IMAP connections (default: false — just checks env vars)",
428
+ },
429
+ generate_config: {
430
+ type: "boolean",
431
+ description: "Generate MCP config snippet with env vars for Claude Code / Cursor (default: true)",
432
+ },
433
+ },
434
+ required: [],
435
+ },
436
+ handler: async (args) => {
437
+ const testConnection = args.test_connection === true;
438
+ const generateConfig = args.generate_config !== false;
439
+ // ── Check env vars ──
440
+ const user = process.env.EMAIL_USER;
441
+ const pass = process.env.EMAIL_PASS;
442
+ const smtpHost = process.env.EMAIL_SMTP_HOST;
443
+ const smtpPort = process.env.EMAIL_SMTP_PORT;
444
+ const imapHost = process.env.EMAIL_IMAP_HOST;
445
+ const imapPort = process.env.EMAIL_IMAP_PORT;
446
+ const checks = [];
447
+ checks.push({
448
+ item: "EMAIL_USER",
449
+ status: user ? "ok" : "missing",
450
+ value: user ? `${user.substring(0, 3)}***` : undefined,
451
+ hint: !user ? "Your email address (e.g., agent@gmail.com)" : undefined,
452
+ });
453
+ checks.push({
454
+ item: "EMAIL_PASS",
455
+ status: pass ? "ok" : "missing",
456
+ hint: !pass ? "App password (NOT your regular password). See setup instructions below." : undefined,
457
+ });
458
+ checks.push({
459
+ item: "EMAIL_SMTP_HOST",
460
+ status: smtpHost ? "ok" : "optional",
461
+ value: smtpHost || "(default: smtp.gmail.com)",
462
+ hint: "Only needed for non-Gmail providers",
463
+ });
464
+ checks.push({
465
+ item: "EMAIL_SMTP_PORT",
466
+ status: smtpPort ? "ok" : "optional",
467
+ value: smtpPort || "(default: 465)",
468
+ });
469
+ checks.push({
470
+ item: "EMAIL_IMAP_HOST",
471
+ status: imapHost ? "ok" : "optional",
472
+ value: imapHost || "(default: imap.gmail.com)",
473
+ hint: "Only needed for non-Gmail providers",
474
+ });
475
+ checks.push({
476
+ item: "EMAIL_IMAP_PORT",
477
+ status: imapPort ? "ok" : "optional",
478
+ value: imapPort || "(default: 993)",
479
+ });
480
+ const ready = checks.filter((c) => c.status === "missing").length === 0;
481
+ // ── Auto-detect provider ──
482
+ let provider = args.provider;
483
+ if (!provider && user) {
484
+ if (user.includes("gmail"))
485
+ provider = "gmail";
486
+ else if (user.includes("outlook") || user.includes("hotmail") || user.includes("live"))
487
+ provider = "outlook";
488
+ else if (user.includes("yahoo"))
489
+ provider = "yahoo";
490
+ else
491
+ provider = "custom";
492
+ }
493
+ if (!provider)
494
+ provider = "gmail";
495
+ // ── Provider-specific setup instructions ──
496
+ const providerGuides = {
497
+ gmail: {
498
+ name: "Gmail",
499
+ smtp: "smtp.gmail.com:465",
500
+ imap: "imap.gmail.com:993",
501
+ steps: [
502
+ "1. Go to https://myaccount.google.com/security",
503
+ "2. Enable 2-Step Verification (required for App Passwords)",
504
+ "3. Go to https://myaccount.google.com/apppasswords",
505
+ "4. Select 'Other (Custom name)', enter 'NodeBench MCP', click Generate",
506
+ "5. Copy the 16-character password (spaces are OK to remove)",
507
+ "6. Set EMAIL_USER=your.email@gmail.com",
508
+ "7. Set EMAIL_PASS=your-16-char-app-password",
509
+ "8. Enable IMAP: Gmail Settings → Forwarding and POP/IMAP → Enable IMAP",
510
+ ],
511
+ },
512
+ outlook: {
513
+ name: "Outlook / Hotmail / Live",
514
+ smtp: "smtp-mail.outlook.com:587",
515
+ imap: "outlook.office365.com:993",
516
+ steps: [
517
+ "1. Go to https://account.microsoft.com/security",
518
+ "2. Enable two-step verification",
519
+ "3. Go to https://account.microsoft.com/security → App passwords",
520
+ "4. Create a new app password, copy it",
521
+ "5. Set EMAIL_USER=your.email@outlook.com",
522
+ "6. Set EMAIL_PASS=your-app-password",
523
+ "7. Set EMAIL_SMTP_HOST=smtp-mail.outlook.com",
524
+ "8. Set EMAIL_SMTP_PORT=587",
525
+ "9. Set EMAIL_IMAP_HOST=outlook.office365.com",
526
+ "Note: Outlook uses STARTTLS on port 587 (not implicit TLS on 465). NodeBench defaults to Gmail's port 465. You MUST set EMAIL_SMTP_PORT=587 and EMAIL_SMTP_HOST.",
527
+ ],
528
+ },
529
+ yahoo: {
530
+ name: "Yahoo Mail",
531
+ smtp: "smtp.mail.yahoo.com:465",
532
+ imap: "imap.mail.yahoo.com:993",
533
+ steps: [
534
+ "1. Go to https://login.yahoo.com/account/security",
535
+ "2. Enable two-step verification",
536
+ "3. Generate an app password: Account Security → Generate app password",
537
+ "4. Set EMAIL_USER=your.email@yahoo.com",
538
+ "5. Set EMAIL_PASS=your-app-password",
539
+ "6. Set EMAIL_SMTP_HOST=smtp.mail.yahoo.com",
540
+ "7. Set EMAIL_IMAP_HOST=imap.mail.yahoo.com",
541
+ ],
542
+ },
543
+ custom: {
544
+ name: "Custom SMTP/IMAP",
545
+ smtp: "(set EMAIL_SMTP_HOST and EMAIL_SMTP_PORT)",
546
+ imap: "(set EMAIL_IMAP_HOST and EMAIL_IMAP_PORT)",
547
+ steps: [
548
+ "1. Get your SMTP server hostname and port from your email provider",
549
+ "2. Get your IMAP server hostname and port from your email provider",
550
+ "3. Set EMAIL_USER=your email address",
551
+ "4. Set EMAIL_PASS=your password or app password",
552
+ "5. Set EMAIL_SMTP_HOST=your.smtp.server",
553
+ "6. Set EMAIL_SMTP_PORT=465 (or 587 for STARTTLS)",
554
+ "7. Set EMAIL_IMAP_HOST=your.imap.server",
555
+ "8. Set EMAIL_IMAP_PORT=993",
556
+ "Note: NodeBench uses implicit TLS (port 465). If your provider requires STARTTLS (port 587), it may not work yet.",
557
+ ],
558
+ },
559
+ };
560
+ const guide = providerGuides[provider];
561
+ // ── Connection test ──
562
+ let smtpTest = null;
563
+ let imapTest = null;
564
+ if (testConnection && ready) {
565
+ // Test SMTP
566
+ try {
567
+ const config = getSmtpConfig();
568
+ const socket = tls.connect({ host: config.host, port: config.port, rejectUnauthorized: true });
569
+ await new Promise((resolve, reject) => {
570
+ socket.once("secureConnect", resolve);
571
+ socket.once("error", reject);
572
+ });
573
+ const greeting = await readSmtp(socket, 5000);
574
+ await smtpCmd(socket, "EHLO nodebench-setup-check");
575
+ await smtpCmd(socket, "AUTH LOGIN");
576
+ await smtpCmd(socket, Buffer.from(config.user).toString("base64"));
577
+ await smtpCmd(socket, Buffer.from(config.pass).toString("base64"));
578
+ await smtpCmd(socket, "QUIT").catch(() => { });
579
+ socket.destroy();
580
+ smtpTest = { status: "ok", message: `SMTP connected and authenticated. Server: ${greeting.split("\r\n")[0]}` };
581
+ }
582
+ catch (e) {
583
+ smtpTest = { status: "error", message: `SMTP failed: ${e.message}` };
584
+ }
585
+ // Test IMAP
586
+ try {
587
+ const config = getImapConfig();
588
+ const socket = tls.connect({ host: config.host, port: config.port, rejectUnauthorized: true });
589
+ await new Promise((resolve, reject) => {
590
+ socket.once("secureConnect", resolve);
591
+ socket.once("error", reject);
592
+ });
593
+ const greeting = await readImapGreeting(socket);
594
+ socket.write('A001 LOGIN "' + config.user + '" "' + config.pass + '"\r\n');
595
+ await readImap(socket, "A001");
596
+ socket.write("A002 LIST \"\" \"*\"\r\n");
597
+ const listResult = await readImap(socket, "A002");
598
+ const folders = listResult.split("\r\n")
599
+ .filter((l) => l.startsWith("* LIST"))
600
+ .map((l) => {
601
+ const m = l.match(/"([^"]+)"$/);
602
+ return m ? m[1] : l;
603
+ });
604
+ socket.write("A003 LOGOUT\r\n");
605
+ socket.destroy();
606
+ imapTest = { status: "ok", message: `IMAP connected. ${folders.length} folders: ${folders.slice(0, 8).join(", ")}` };
607
+ }
608
+ catch (e) {
609
+ imapTest = { status: "error", message: `IMAP failed: ${e.message}` };
610
+ }
611
+ }
612
+ // ── Generate MCP config snippet ──
613
+ let configSnippet = null;
614
+ if (generateConfig) {
615
+ configSnippet = JSON.stringify({
616
+ mcpServers: {
617
+ nodebench: {
618
+ command: "npx",
619
+ args: ["-y", "nodebench-mcp"],
620
+ env: {
621
+ EMAIL_USER: user || "your.email@gmail.com",
622
+ EMAIL_PASS: pass ? "(already set)" : "your-16-char-app-password",
623
+ ...(provider !== "gmail" ? {
624
+ EMAIL_SMTP_HOST: smtpHost || guide.smtp.split(":")[0],
625
+ EMAIL_SMTP_PORT: smtpPort || guide.smtp.split(":")[1],
626
+ EMAIL_IMAP_HOST: imapHost || guide.imap.split(":")[0],
627
+ EMAIL_IMAP_PORT: imapPort || guide.imap.split(":")[1],
628
+ } : {}),
629
+ },
630
+ },
631
+ },
632
+ }, null, 2);
633
+ }
634
+ return [
635
+ {
636
+ type: "text",
637
+ text: JSON.stringify({
638
+ ready,
639
+ provider: guide.name,
640
+ checks,
641
+ ...(ready ? {} : { setupInstructions: guide.steps }),
642
+ ...(smtpTest ? { smtpConnectionTest: smtpTest } : {}),
643
+ ...(imapTest ? { imapConnectionTest: imapTest } : {}),
644
+ ...(configSnippet ? { mcpConfigSnippet: configSnippet } : {}),
645
+ nextSteps: ready
646
+ ? [
647
+ "Email is configured! Try: send_email to send, read_emails to read inbox.",
648
+ "Run with test_connection=true to verify SMTP/IMAP connectivity.",
649
+ "Use get_workflow_chain('email_assistant') for the full workflow.",
650
+ "Use get_workflow_chain('research_digest') to set up automated research digests.",
651
+ ]
652
+ : [
653
+ `Follow the ${guide.name} setup instructions above.`,
654
+ "Set the env vars in your MCP config (see mcpConfigSnippet) or shell profile.",
655
+ "Re-run check_email_setup to verify.",
656
+ "Then run with test_connection=true to test SMTP/IMAP.",
657
+ ],
658
+ }),
659
+ },
660
+ ];
661
+ },
662
+ },
663
+ ];
664
+ //# sourceMappingURL=emailTools.js.map