@luckydraw/cumulus 0.27.41 → 0.28.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 (99) hide show
  1. package/dist/gateway/adapters/webchat.d.ts +5 -7
  2. package/dist/gateway/adapters/webchat.d.ts.map +1 -1
  3. package/dist/gateway/adapters/webchat.js +204 -57
  4. package/dist/gateway/adapters/webchat.js.map +1 -1
  5. package/dist/gateway/config.d.ts +48 -0
  6. package/dist/gateway/config.d.ts.map +1 -1
  7. package/dist/gateway/config.js +90 -0
  8. package/dist/gateway/config.js.map +1 -1
  9. package/dist/gateway/daemon.d.ts.map +1 -1
  10. package/dist/gateway/daemon.js +268 -27
  11. package/dist/gateway/daemon.js.map +1 -1
  12. package/dist/gateway/gateway-agents-mcp.js +570 -31
  13. package/dist/gateway/gateway-agents-mcp.js.map +1 -1
  14. package/dist/gateway/hooks.d.ts +101 -0
  15. package/dist/gateway/hooks.d.ts.map +1 -0
  16. package/dist/gateway/hooks.js +200 -0
  17. package/dist/gateway/hooks.js.map +1 -0
  18. package/dist/gateway/scheduler.d.ts +50 -0
  19. package/dist/gateway/scheduler.d.ts.map +1 -0
  20. package/dist/gateway/scheduler.js +263 -0
  21. package/dist/gateway/scheduler.js.map +1 -0
  22. package/dist/gateway/server.d.ts +16 -1
  23. package/dist/gateway/server.d.ts.map +1 -1
  24. package/dist/gateway/server.js +451 -22
  25. package/dist/gateway/server.js.map +1 -1
  26. package/dist/gateway/static/blex.min.js +74 -17
  27. package/dist/gateway/static/chat.html +1 -4
  28. package/dist/gateway/static/dashboard.html +151 -0
  29. package/dist/gateway/static/dashboard.js +229 -0
  30. package/dist/gateway/static/widget.js +656 -460
  31. package/dist/index.d.ts +13 -5
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +9 -3
  34. package/dist/index.js.map +1 -1
  35. package/dist/lib/agentic-loop.d.ts +124 -0
  36. package/dist/lib/agentic-loop.d.ts.map +1 -0
  37. package/dist/lib/agentic-loop.js +338 -0
  38. package/dist/lib/agentic-loop.js.map +1 -0
  39. package/dist/lib/config.d.ts +28 -15
  40. package/dist/lib/config.d.ts.map +1 -1
  41. package/dist/lib/config.js +47 -41
  42. package/dist/lib/config.js.map +1 -1
  43. package/dist/lib/gateway.d.ts +34 -3
  44. package/dist/lib/gateway.d.ts.map +1 -1
  45. package/dist/lib/gateway.js +584 -37
  46. package/dist/lib/gateway.js.map +1 -1
  47. package/dist/lib/huggingface-provider.d.ts +26 -0
  48. package/dist/lib/huggingface-provider.d.ts.map +1 -0
  49. package/dist/lib/huggingface-provider.js +338 -0
  50. package/dist/lib/huggingface-provider.js.map +1 -0
  51. package/dist/lib/provider.d.ts +71 -0
  52. package/dist/lib/provider.d.ts.map +1 -0
  53. package/dist/lib/provider.js +90 -0
  54. package/dist/lib/provider.js.map +1 -0
  55. package/dist/lib/tools/ask-user-question.d.ts +14 -0
  56. package/dist/lib/tools/ask-user-question.d.ts.map +1 -0
  57. package/dist/lib/tools/ask-user-question.js +74 -0
  58. package/dist/lib/tools/ask-user-question.js.map +1 -0
  59. package/dist/lib/tools/bash.d.ts +6 -0
  60. package/dist/lib/tools/bash.d.ts.map +1 -0
  61. package/dist/lib/tools/bash.js +85 -0
  62. package/dist/lib/tools/bash.js.map +1 -0
  63. package/dist/lib/tools/edit.d.ts +6 -0
  64. package/dist/lib/tools/edit.d.ts.map +1 -0
  65. package/dist/lib/tools/edit.js +79 -0
  66. package/dist/lib/tools/edit.js.map +1 -0
  67. package/dist/lib/tools/glob.d.ts +6 -0
  68. package/dist/lib/tools/glob.d.ts.map +1 -0
  69. package/dist/lib/tools/glob.js +91 -0
  70. package/dist/lib/tools/glob.js.map +1 -0
  71. package/dist/lib/tools/grep.d.ts +6 -0
  72. package/dist/lib/tools/grep.d.ts.map +1 -0
  73. package/dist/lib/tools/grep.js +110 -0
  74. package/dist/lib/tools/grep.js.map +1 -0
  75. package/dist/lib/tools/index.d.ts +29 -0
  76. package/dist/lib/tools/index.d.ts.map +1 -0
  77. package/dist/lib/tools/index.js +52 -0
  78. package/dist/lib/tools/index.js.map +1 -0
  79. package/dist/lib/tools/read.d.ts +6 -0
  80. package/dist/lib/tools/read.d.ts.map +1 -0
  81. package/dist/lib/tools/read.js +80 -0
  82. package/dist/lib/tools/read.js.map +1 -0
  83. package/dist/lib/tools/tool-search.d.ts +30 -0
  84. package/dist/lib/tools/tool-search.d.ts.map +1 -0
  85. package/dist/lib/tools/tool-search.js +108 -0
  86. package/dist/lib/tools/tool-search.js.map +1 -0
  87. package/dist/lib/tools/web-fetch.d.ts +9 -0
  88. package/dist/lib/tools/web-fetch.d.ts.map +1 -0
  89. package/dist/lib/tools/web-fetch.js +114 -0
  90. package/dist/lib/tools/web-fetch.js.map +1 -0
  91. package/dist/lib/tools/web-search.d.ts +12 -0
  92. package/dist/lib/tools/web-search.d.ts.map +1 -0
  93. package/dist/lib/tools/web-search.js +89 -0
  94. package/dist/lib/tools/web-search.js.map +1 -0
  95. package/dist/lib/tools/write.d.ts +6 -0
  96. package/dist/lib/tools/write.d.ts.map +1 -0
  97. package/dist/lib/tools/write.js +44 -0
  98. package/dist/lib/tools/write.js.map +1 -0
  99. package/package.json +3 -2
@@ -25,6 +25,68 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
25
25
  const API_URL = process.env.GATEWAY_API_URL || 'http://127.0.0.1:8090';
26
26
  const API_KEY = process.env.GATEWAY_API_KEY || '';
27
27
  const AGENT_NAME = process.env.GATEWAY_AGENT_NAME || 'unknown';
28
+ const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
29
+ const RESEND_DEFAULT_FROM = process.env.RESEND_DEFAULT_FROM || '';
30
+ const RESEND_REPLY_TO = process.env.RESEND_REPLY_TO || '';
31
+ const RESEND_RATE_LIMIT = parseInt(process.env.RESEND_RATE_LIMIT || '10', 10);
32
+ // Rate limiting: track emails sent per thread per hour
33
+ const emailRateLimits = new Map();
34
+ function checkRateLimit(threadName) {
35
+ const now = Date.now();
36
+ const entry = emailRateLimits.get(threadName);
37
+ if (!entry || now > entry.resetAt) {
38
+ emailRateLimits.set(threadName, { count: 1, resetAt: now + 3600_000 });
39
+ return true;
40
+ }
41
+ if (entry.count >= RESEND_RATE_LIMIT) {
42
+ return false;
43
+ }
44
+ entry.count++;
45
+ return true;
46
+ }
47
+ /**
48
+ * Make an HTTPS request to the Resend API.
49
+ */
50
+ async function resendRequest(method, urlPath, body) {
51
+ const { default: https } = await import('https');
52
+ return new Promise(resolve => {
53
+ const options = {
54
+ hostname: 'api.resend.com',
55
+ port: 443,
56
+ path: urlPath,
57
+ method,
58
+ headers: {
59
+ 'Content-Type': 'application/json',
60
+ Authorization: `Bearer ${RESEND_API_KEY}`,
61
+ },
62
+ };
63
+ const req = https.request(options, res => {
64
+ let data = '';
65
+ res.on('data', (chunk) => (data += chunk.toString()));
66
+ res.on('end', () => {
67
+ try {
68
+ resolve({
69
+ status: res.statusCode || 500,
70
+ data: JSON.parse(data),
71
+ });
72
+ }
73
+ catch {
74
+ resolve({
75
+ status: res.statusCode || 500,
76
+ data: { error: 'Invalid JSON response from Resend' },
77
+ });
78
+ }
79
+ });
80
+ });
81
+ req.on('error', (err) => {
82
+ resolve({ status: 0, data: { error: `Resend API request failed: ${err.message}` } });
83
+ });
84
+ if (body) {
85
+ req.write(JSON.stringify(body));
86
+ }
87
+ req.end();
88
+ });
89
+ }
28
90
  /**
29
91
  * Make an HTTP request to the gateway API and return the parsed JSON response.
30
92
  */
@@ -62,6 +124,42 @@ function apiRequest(method, urlPath, body) {
62
124
  req.end();
63
125
  });
64
126
  }
127
+ /**
128
+ * Upload a file to the gateway media directory via the upload API.
129
+ */
130
+ function uploadFile(data, filename) {
131
+ return new Promise(resolve => {
132
+ const url = new URL(`/api/media/upload?filename=${encodeURIComponent(filename)}`, API_URL);
133
+ const options = {
134
+ hostname: url.hostname,
135
+ port: url.port,
136
+ path: url.pathname + url.search,
137
+ method: 'POST',
138
+ headers: {
139
+ 'Content-Type': 'application/octet-stream',
140
+ 'Content-Length': data.length,
141
+ 'X-API-Key': API_KEY,
142
+ },
143
+ };
144
+ const req = http.request(options, res => {
145
+ let respData = '';
146
+ res.on('data', (chunk) => (respData += chunk.toString()));
147
+ res.on('end', () => {
148
+ try {
149
+ resolve(JSON.parse(respData));
150
+ }
151
+ catch {
152
+ resolve({ error: 'Invalid JSON response from upload' });
153
+ }
154
+ });
155
+ });
156
+ req.on('error', (err) => {
157
+ resolve({ error: `Upload failed: ${err.message}` });
158
+ });
159
+ req.write(data);
160
+ req.end();
161
+ });
162
+ }
65
163
  async function main() {
66
164
  const server = new Server({ name: 'gateway-agents', version: '1.0.0' }, { capabilities: { tools: {} } });
67
165
  // List available tools
@@ -114,43 +212,209 @@ async function main() {
114
212
  required: ['target', 'message'],
115
213
  },
116
214
  },
215
+ // broadcast tool removed — agents must use send_to_agent with specific targets
117
216
  {
118
- name: 'broadcast',
119
- description: 'Send a message to ALL active agents simultaneously (except yourself). Busy agents will have their messages queued for batch delivery when they finish.',
217
+ name: 'notify_user',
218
+ description: "Send a push notification to the user's phone/device. Use this when you need to alert the user about something important — task completion, errors, questions that need human input, or anything worth interrupting them for. The notification appears even if they're not looking at the chat.",
120
219
  inputSchema: {
121
220
  type: 'object',
122
221
  properties: {
123
222
  message: {
124
223
  type: 'string',
125
- description: 'The message to broadcast to all agents.',
224
+ description: 'The notification body text. Keep it concise (1-2 sentences). Will be shown as the notification body.',
225
+ },
226
+ title: {
227
+ type: 'string',
228
+ description: 'Optional notification title. Defaults to your agent name. Keep short (under 50 chars).',
229
+ },
230
+ priority: {
231
+ type: 'string',
232
+ enum: ['low', 'normal', 'high'],
233
+ description: 'Notification priority. "high" for urgent alerts, "normal" for regular updates, "low" for FYI. Default: "normal".',
126
234
  },
127
235
  },
128
236
  required: ['message'],
129
237
  },
130
238
  },
131
239
  {
132
- name: 'notify_user',
133
- description: "Send a push notification to the user's phone/device. Use this when you need to alert the user about something important task completion, errors, questions that need human input, or anything worth interrupting them for. The notification appears even if they're not looking at the chat.",
240
+ name: 'upload_media',
241
+ description: 'Upload a file to the gateway media directory so it can be served via a public URL. Use this to make locally-generated files (screenshots, diagrams, exports) accessible in blex blocks (image, gallery) or as download links. Returns the public URL path (e.g., /media/filename.png). The file is written to ~/.cumulus/media/ and served at /media/* without authentication.',
242
+ inputSchema: {
243
+ type: 'object',
244
+ properties: {
245
+ source_path: {
246
+ type: 'string',
247
+ description: 'Absolute path to a local file to upload. The file will be copied to the media directory.',
248
+ },
249
+ filename: {
250
+ type: 'string',
251
+ description: "Optional: desired filename (with extension). If omitted, uses the source file's name. A content hash is appended to prevent collisions.",
252
+ },
253
+ },
254
+ required: ['source_path'],
255
+ },
256
+ },
257
+ {
258
+ name: 'schedule_trigger',
259
+ description: 'Schedule a future message injection into this thread. Use this for follow-up reminders, drip sequences, periodic check-ins, or any deferred action. The message is injected as a system-level prompt at the scheduled time, triggering a new AI turn.',
134
260
  inputSchema: {
135
261
  type: 'object',
136
262
  properties: {
263
+ id: {
264
+ type: 'string',
265
+ description: 'Unique schedule ID (e.g., "day3-followup", "weekly-check"). Used to cancel or reference the schedule.',
266
+ },
267
+ trigger: {
268
+ type: 'string',
269
+ enum: ['once', 'cron'],
270
+ description: '"once" fires at a specific datetime then auto-removes. "cron" fires on a recurring schedule.',
271
+ },
272
+ at: {
273
+ type: 'string',
274
+ description: 'ISO 8601 datetime for "once" triggers (e.g., "2026-04-06T09:00:00-04:00"). Required for "once", ignored for "cron".',
275
+ },
276
+ cron: {
277
+ type: 'string',
278
+ description: 'Cron expression for "cron" triggers (e.g., "0 9 * * MON" for every Monday at 9am). Five fields: minute hour day-of-month month day-of-week.',
279
+ },
137
280
  message: {
138
281
  type: 'string',
139
- description: 'The notification body text. Keep it concise (1-2 sentences). Will be shown as the notification body.',
282
+ description: 'The message to inject when the trigger fires. Should describe what action to take.',
140
283
  },
141
- title: {
284
+ },
285
+ required: ['id', 'trigger', 'message'],
286
+ },
287
+ },
288
+ {
289
+ name: 'cancel_schedule',
290
+ description: 'Cancel a scheduled trigger by ID. The schedule is removed from the thread config and will no longer fire.',
291
+ inputSchema: {
292
+ type: 'object',
293
+ properties: {
294
+ id: {
142
295
  type: 'string',
143
- description: 'Optional notification title. Defaults to your agent name. Keep short (under 50 chars).',
296
+ description: 'The schedule ID to cancel.',
144
297
  },
145
- priority: {
298
+ },
299
+ required: ['id'],
300
+ },
301
+ },
302
+ {
303
+ name: 'list_schedules',
304
+ description: 'List all scheduled triggers for this thread. Returns the schedule ID, trigger type, timing, and message for each.',
305
+ inputSchema: {
306
+ type: 'object',
307
+ properties: {},
308
+ required: [],
309
+ },
310
+ },
311
+ {
312
+ name: 'update_pipeline',
313
+ description: "Update pipeline metadata for this thread or another thread. Use for stage changes, score updates, and tracking next actions. The orchestrator agent uses the threadName parameter to update other threads' pipeline state.",
314
+ inputSchema: {
315
+ type: 'object',
316
+ properties: {
317
+ threadName: {
146
318
  type: 'string',
147
- enum: ['low', 'normal', 'high'],
148
- description: 'Notification priority. "high" for urgent alerts, "normal" for regular updates, "low" for FYI. Default: "normal".',
319
+ description: 'Thread to update (defaults to current thread). Use for orchestrator cross-thread updates.',
320
+ },
321
+ stage: {
322
+ type: 'string',
323
+ enum: [
324
+ 'prospect',
325
+ 'outreach',
326
+ 'warm-engagement',
327
+ 'proposal',
328
+ 'onboarding',
329
+ 'active-client',
330
+ 'archived',
331
+ ],
332
+ description: 'Pipeline stage.',
333
+ },
334
+ score: {
335
+ type: 'number',
336
+ description: 'Engagement score (0-100).',
337
+ },
338
+ lastContact: {
339
+ type: 'string',
340
+ description: 'ISO timestamp of last contact.',
341
+ },
342
+ nextAction: {
343
+ type: 'string',
344
+ description: 'Human-readable next action.',
345
+ },
346
+ nextActionDate: {
347
+ type: 'string',
348
+ description: 'ISO timestamp when next action should be taken.',
149
349
  },
150
350
  },
151
- required: ['message'],
351
+ required: [],
152
352
  },
153
353
  },
354
+ ...(RESEND_API_KEY
355
+ ? [
356
+ {
357
+ name: 'send_email',
358
+ description: 'Send an email via Resend API. Use this for outreach, follow-ups, and notifications. All sends are logged to the thread history and rate-limited. The default sender is configured in the gateway config.',
359
+ inputSchema: {
360
+ type: 'object',
361
+ properties: {
362
+ to: {
363
+ type: 'string',
364
+ description: 'Recipient email address.',
365
+ },
366
+ subject: {
367
+ type: 'string',
368
+ description: 'Email subject line.',
369
+ },
370
+ body: {
371
+ type: 'string',
372
+ description: 'Email body in plain text. For HTML emails, also provide the html parameter.',
373
+ },
374
+ from: {
375
+ type: 'string',
376
+ description: `Optional: sender address. Defaults to "${RESEND_DEFAULT_FROM}".`,
377
+ },
378
+ replyTo: {
379
+ type: 'string',
380
+ description: `Optional: reply-to address. Defaults to "${RESEND_REPLY_TO || RESEND_DEFAULT_FROM}".`,
381
+ },
382
+ html: {
383
+ type: 'string',
384
+ description: 'Optional: HTML body. If provided, used as the email body with plain text as fallback.',
385
+ },
386
+ tags: {
387
+ type: 'array',
388
+ items: {
389
+ type: 'object',
390
+ properties: {
391
+ name: { type: 'string' },
392
+ value: { type: 'string' },
393
+ },
394
+ required: ['name', 'value'],
395
+ },
396
+ description: 'Optional: tags for tracking/filtering (e.g., [{"name":"campaign","value":"outreach-q2"}]).',
397
+ },
398
+ },
399
+ required: ['to', 'subject', 'body'],
400
+ },
401
+ },
402
+ {
403
+ name: 'list_emails',
404
+ description: 'List recently sent emails from the Resend API. Use this to check delivery status, review what was sent, or track engagement.',
405
+ inputSchema: {
406
+ type: 'object',
407
+ properties: {
408
+ limit: {
409
+ type: 'number',
410
+ description: 'Maximum number of emails to return (default: 10, max: 100).',
411
+ },
412
+ },
413
+ required: [],
414
+ },
415
+ },
416
+ ]
417
+ : []),
154
418
  ],
155
419
  }));
156
420
  // Handle tool calls
@@ -221,56 +485,331 @@ async function main() {
221
485
  isError: !!result.error,
222
486
  };
223
487
  }
224
- if (name === 'broadcast') {
488
+ // broadcast handler removed — agents must use send_to_agent with specific targets
489
+ if (name === 'notify_user') {
225
490
  const message = args?.message;
491
+ const title = args?.title || AGENT_NAME;
492
+ const priority = args?.priority || 'normal';
226
493
  if (!message) {
227
494
  return {
228
495
  content: [
229
496
  {
230
497
  type: 'text',
231
- text: JSON.stringify({ delivered: false, error: 'Missing message' }),
498
+ text: JSON.stringify({ sent: false, error: 'Missing message' }),
232
499
  },
233
500
  ],
234
501
  isError: true,
235
502
  };
236
503
  }
237
- const result = await apiRequest('POST', '/api/agents/inject', {
238
- targets: '__broadcast__',
239
- message,
240
- sender: AGENT_NAME,
241
- messageType: 'broadcast',
504
+ const result = await apiRequest('POST', '/api/notify', {
505
+ title,
506
+ body: message,
507
+ threadName: AGENT_NAME,
508
+ priority,
242
509
  });
243
510
  return {
244
511
  content: [{ type: 'text', text: JSON.stringify(result) }],
245
512
  isError: !!result.error,
246
513
  };
247
514
  }
248
- if (name === 'notify_user') {
249
- const message = args?.message;
250
- const title = args?.title || AGENT_NAME;
251
- const priority = args?.priority || 'normal';
252
- if (!message) {
515
+ if (name === 'upload_media') {
516
+ const sourcePath = args?.source_path;
517
+ const filename = args?.filename;
518
+ if (!sourcePath) {
253
519
  return {
254
520
  content: [
255
521
  {
256
522
  type: 'text',
257
- text: JSON.stringify({ sent: false, error: 'Missing message' }),
523
+ text: JSON.stringify({ error: 'Missing source_path' }),
258
524
  },
259
525
  ],
260
526
  isError: true,
261
527
  };
262
528
  }
263
- const result = await apiRequest('POST', '/api/notify', {
264
- title,
265
- body: message,
266
- threadName: AGENT_NAME,
267
- priority,
268
- });
529
+ const fs = await import('fs');
530
+ const nodePath = await import('path');
531
+ let fileData;
532
+ try {
533
+ fileData = fs.readFileSync(sourcePath);
534
+ }
535
+ catch (err) {
536
+ return {
537
+ content: [
538
+ {
539
+ type: 'text',
540
+ text: JSON.stringify({
541
+ error: `Cannot read file: ${err instanceof Error ? err.message : String(err)}`,
542
+ }),
543
+ },
544
+ ],
545
+ isError: true,
546
+ };
547
+ }
548
+ const uploadFilename = filename || nodePath.basename(sourcePath);
549
+ const result = await uploadFile(fileData, uploadFilename);
269
550
  return {
270
551
  content: [{ type: 'text', text: JSON.stringify(result) }],
271
552
  isError: !!result.error,
272
553
  };
273
554
  }
555
+ if (name === 'send_email') {
556
+ if (!RESEND_API_KEY) {
557
+ return {
558
+ content: [
559
+ {
560
+ type: 'text',
561
+ text: JSON.stringify({
562
+ error: 'Email not configured — resend.apiKey missing from gateway config',
563
+ }),
564
+ },
565
+ ],
566
+ isError: true,
567
+ };
568
+ }
569
+ const to = args?.to;
570
+ const subject = args?.subject;
571
+ const body = args?.body;
572
+ const from = args?.from || RESEND_DEFAULT_FROM;
573
+ const replyTo = args?.replyTo || RESEND_REPLY_TO || from;
574
+ const html = args?.html;
575
+ const tags = args?.tags;
576
+ if (!to || !subject || !body) {
577
+ return {
578
+ content: [
579
+ {
580
+ type: 'text',
581
+ text: JSON.stringify({ error: 'Missing required fields: to, subject, body' }),
582
+ },
583
+ ],
584
+ isError: true,
585
+ };
586
+ }
587
+ // Rate limit check
588
+ if (!checkRateLimit(AGENT_NAME)) {
589
+ return {
590
+ content: [
591
+ {
592
+ type: 'text',
593
+ text: JSON.stringify({
594
+ error: `Rate limit exceeded: max ${RESEND_RATE_LIMIT} emails per hour per thread`,
595
+ }),
596
+ },
597
+ ],
598
+ isError: true,
599
+ };
600
+ }
601
+ const emailPayload = {
602
+ from,
603
+ to: [to],
604
+ subject,
605
+ text: body,
606
+ };
607
+ if (replyTo)
608
+ emailPayload.reply_to = replyTo;
609
+ if (html)
610
+ emailPayload.html = html;
611
+ if (tags && tags.length > 0)
612
+ emailPayload.tags = tags;
613
+ const result = await resendRequest('POST', '/emails', emailPayload);
614
+ if (result.status >= 200 && result.status < 300) {
615
+ // Notify user on first email from this thread
616
+ const rateEntry = emailRateLimits.get(AGENT_NAME);
617
+ if (rateEntry && rateEntry.count === 1) {
618
+ await apiRequest('POST', '/api/notify', {
619
+ title: `${AGENT_NAME}: First email sent`,
620
+ body: `Sent to ${to}: "${subject}"`,
621
+ threadName: AGENT_NAME,
622
+ priority: 'normal',
623
+ });
624
+ }
625
+ return {
626
+ content: [
627
+ {
628
+ type: 'text',
629
+ text: JSON.stringify({
630
+ sent: true,
631
+ id: result.data.id,
632
+ from,
633
+ to,
634
+ subject,
635
+ sentAt: new Date().toISOString(),
636
+ }),
637
+ },
638
+ ],
639
+ };
640
+ }
641
+ return {
642
+ content: [
643
+ {
644
+ type: 'text',
645
+ text: JSON.stringify({
646
+ sent: false,
647
+ error: result.data.message || result.data.error || `Resend API returned ${result.status}`,
648
+ }),
649
+ },
650
+ ],
651
+ isError: true,
652
+ };
653
+ }
654
+ if (name === 'list_emails') {
655
+ if (!RESEND_API_KEY) {
656
+ return {
657
+ content: [
658
+ {
659
+ type: 'text',
660
+ text: JSON.stringify({
661
+ error: 'Email not configured — resend.apiKey missing from gateway config',
662
+ }),
663
+ },
664
+ ],
665
+ isError: true,
666
+ };
667
+ }
668
+ const limit = Math.min(Math.max(args?.limit || 10, 1), 100);
669
+ const result = await resendRequest('GET', `/emails?limit=${limit}`);
670
+ if (result.status >= 200 && result.status < 300) {
671
+ return {
672
+ content: [{ type: 'text', text: JSON.stringify(result.data) }],
673
+ };
674
+ }
675
+ return {
676
+ content: [
677
+ {
678
+ type: 'text',
679
+ text: JSON.stringify({
680
+ error: result.data.message || result.data.error || `Resend API returned ${result.status}`,
681
+ }),
682
+ },
683
+ ],
684
+ isError: true,
685
+ };
686
+ }
687
+ if (name === 'schedule_trigger') {
688
+ const id = args?.id;
689
+ const trigger = args?.trigger;
690
+ const at = args?.at;
691
+ const cron = args?.cron;
692
+ const message = args?.message;
693
+ if (!id || !trigger || !message) {
694
+ return {
695
+ content: [
696
+ { type: 'text', text: 'Missing required fields: id, trigger, message' },
697
+ ],
698
+ isError: true,
699
+ };
700
+ }
701
+ if (trigger === 'once' && !at) {
702
+ return {
703
+ content: [{ type: 'text', text: 'Once trigger requires "at" datetime field' }],
704
+ isError: true,
705
+ };
706
+ }
707
+ if (trigger === 'cron' && !cron) {
708
+ return {
709
+ content: [
710
+ { type: 'text', text: 'Cron trigger requires "cron" expression field' },
711
+ ],
712
+ isError: true,
713
+ };
714
+ }
715
+ // Save schedule to thread config via REST API
716
+ const schedule = { id, trigger, ...(at ? { at } : {}), ...(cron ? { cron } : {}), message };
717
+ const result = await apiRequest('POST', `/api/thread/${encodeURIComponent(AGENT_NAME)}/schedules`, schedule);
718
+ if (result.error) {
719
+ return {
720
+ content: [{ type: 'text', text: `Failed to create schedule: ${result.error}` }],
721
+ isError: true,
722
+ };
723
+ }
724
+ return {
725
+ content: [
726
+ {
727
+ type: 'text',
728
+ text: JSON.stringify({
729
+ created: true,
730
+ id,
731
+ trigger,
732
+ ...(at ? { at } : {}),
733
+ ...(cron ? { cron } : {}),
734
+ }),
735
+ },
736
+ ],
737
+ };
738
+ }
739
+ if (name === 'cancel_schedule') {
740
+ const id = args?.id;
741
+ if (!id) {
742
+ return {
743
+ content: [{ type: 'text', text: 'Missing required field: id' }],
744
+ isError: true,
745
+ };
746
+ }
747
+ const result = await apiRequest('DELETE', `/api/thread/${encodeURIComponent(AGENT_NAME)}/schedules/${encodeURIComponent(id)}`);
748
+ if (result.error) {
749
+ return {
750
+ content: [{ type: 'text', text: `Failed to cancel schedule: ${result.error}` }],
751
+ isError: true,
752
+ };
753
+ }
754
+ return {
755
+ content: [{ type: 'text', text: JSON.stringify({ cancelled: true, id }) }],
756
+ };
757
+ }
758
+ if (name === 'list_schedules') {
759
+ const result = await apiRequest('GET', `/api/thread/${encodeURIComponent(AGENT_NAME)}/schedules`);
760
+ if (result.error) {
761
+ return {
762
+ content: [{ type: 'text', text: `Failed to list schedules: ${result.error}` }],
763
+ isError: true,
764
+ };
765
+ }
766
+ return {
767
+ content: [{ type: 'text', text: JSON.stringify(result.data) }],
768
+ };
769
+ }
770
+ if (name === 'update_pipeline') {
771
+ const targetThread = args?.threadName || AGENT_NAME;
772
+ const pipelineFields = {};
773
+ for (const key of ['stage', 'score', 'lastContact', 'nextAction', 'nextActionDate']) {
774
+ if (args?.[key] !== undefined)
775
+ pipelineFields[key] = args[key];
776
+ }
777
+ if (Object.keys(pipelineFields).length === 0) {
778
+ return {
779
+ content: [
780
+ {
781
+ type: 'text',
782
+ text: JSON.stringify({ error: 'No pipeline fields provided to update' }),
783
+ },
784
+ ],
785
+ isError: true,
786
+ };
787
+ }
788
+ const result = await apiRequest('PUT', `/api/thread/${encodeURIComponent(targetThread)}/pipeline`, pipelineFields);
789
+ if (result.error) {
790
+ return {
791
+ content: [
792
+ {
793
+ type: 'text',
794
+ text: JSON.stringify({ error: `Failed to update pipeline: ${result.error}` }),
795
+ },
796
+ ],
797
+ isError: true,
798
+ };
799
+ }
800
+ return {
801
+ content: [
802
+ {
803
+ type: 'text',
804
+ text: JSON.stringify({
805
+ updated: true,
806
+ thread: targetThread,
807
+ pipeline: result.pipeline || pipelineFields,
808
+ }),
809
+ },
810
+ ],
811
+ };
812
+ }
274
813
  return {
275
814
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],
276
815
  isError: true,