@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.
- package/dist/gateway/adapters/webchat.d.ts +5 -7
- package/dist/gateway/adapters/webchat.d.ts.map +1 -1
- package/dist/gateway/adapters/webchat.js +204 -57
- package/dist/gateway/adapters/webchat.js.map +1 -1
- package/dist/gateway/config.d.ts +48 -0
- package/dist/gateway/config.d.ts.map +1 -1
- package/dist/gateway/config.js +90 -0
- package/dist/gateway/config.js.map +1 -1
- package/dist/gateway/daemon.d.ts.map +1 -1
- package/dist/gateway/daemon.js +268 -27
- package/dist/gateway/daemon.js.map +1 -1
- package/dist/gateway/gateway-agents-mcp.js +570 -31
- package/dist/gateway/gateway-agents-mcp.js.map +1 -1
- package/dist/gateway/hooks.d.ts +101 -0
- package/dist/gateway/hooks.d.ts.map +1 -0
- package/dist/gateway/hooks.js +200 -0
- package/dist/gateway/hooks.js.map +1 -0
- package/dist/gateway/scheduler.d.ts +50 -0
- package/dist/gateway/scheduler.d.ts.map +1 -0
- package/dist/gateway/scheduler.js +263 -0
- package/dist/gateway/scheduler.js.map +1 -0
- package/dist/gateway/server.d.ts +16 -1
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/gateway/server.js +451 -22
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/static/blex.min.js +74 -17
- package/dist/gateway/static/chat.html +1 -4
- package/dist/gateway/static/dashboard.html +151 -0
- package/dist/gateway/static/dashboard.js +229 -0
- package/dist/gateway/static/widget.js +656 -460
- package/dist/index.d.ts +13 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/agentic-loop.d.ts +124 -0
- package/dist/lib/agentic-loop.d.ts.map +1 -0
- package/dist/lib/agentic-loop.js +338 -0
- package/dist/lib/agentic-loop.js.map +1 -0
- package/dist/lib/config.d.ts +28 -15
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +47 -41
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/gateway.d.ts +34 -3
- package/dist/lib/gateway.d.ts.map +1 -1
- package/dist/lib/gateway.js +584 -37
- package/dist/lib/gateway.js.map +1 -1
- package/dist/lib/huggingface-provider.d.ts +26 -0
- package/dist/lib/huggingface-provider.d.ts.map +1 -0
- package/dist/lib/huggingface-provider.js +338 -0
- package/dist/lib/huggingface-provider.js.map +1 -0
- package/dist/lib/provider.d.ts +71 -0
- package/dist/lib/provider.d.ts.map +1 -0
- package/dist/lib/provider.js +90 -0
- package/dist/lib/provider.js.map +1 -0
- package/dist/lib/tools/ask-user-question.d.ts +14 -0
- package/dist/lib/tools/ask-user-question.d.ts.map +1 -0
- package/dist/lib/tools/ask-user-question.js +74 -0
- package/dist/lib/tools/ask-user-question.js.map +1 -0
- package/dist/lib/tools/bash.d.ts +6 -0
- package/dist/lib/tools/bash.d.ts.map +1 -0
- package/dist/lib/tools/bash.js +85 -0
- package/dist/lib/tools/bash.js.map +1 -0
- package/dist/lib/tools/edit.d.ts +6 -0
- package/dist/lib/tools/edit.d.ts.map +1 -0
- package/dist/lib/tools/edit.js +79 -0
- package/dist/lib/tools/edit.js.map +1 -0
- package/dist/lib/tools/glob.d.ts +6 -0
- package/dist/lib/tools/glob.d.ts.map +1 -0
- package/dist/lib/tools/glob.js +91 -0
- package/dist/lib/tools/glob.js.map +1 -0
- package/dist/lib/tools/grep.d.ts +6 -0
- package/dist/lib/tools/grep.d.ts.map +1 -0
- package/dist/lib/tools/grep.js +110 -0
- package/dist/lib/tools/grep.js.map +1 -0
- package/dist/lib/tools/index.d.ts +29 -0
- package/dist/lib/tools/index.d.ts.map +1 -0
- package/dist/lib/tools/index.js +52 -0
- package/dist/lib/tools/index.js.map +1 -0
- package/dist/lib/tools/read.d.ts +6 -0
- package/dist/lib/tools/read.d.ts.map +1 -0
- package/dist/lib/tools/read.js +80 -0
- package/dist/lib/tools/read.js.map +1 -0
- package/dist/lib/tools/tool-search.d.ts +30 -0
- package/dist/lib/tools/tool-search.d.ts.map +1 -0
- package/dist/lib/tools/tool-search.js +108 -0
- package/dist/lib/tools/tool-search.js.map +1 -0
- package/dist/lib/tools/web-fetch.d.ts +9 -0
- package/dist/lib/tools/web-fetch.d.ts.map +1 -0
- package/dist/lib/tools/web-fetch.js +114 -0
- package/dist/lib/tools/web-fetch.js.map +1 -0
- package/dist/lib/tools/web-search.d.ts +12 -0
- package/dist/lib/tools/web-search.d.ts.map +1 -0
- package/dist/lib/tools/web-search.js +89 -0
- package/dist/lib/tools/web-search.js.map +1 -0
- package/dist/lib/tools/write.d.ts +6 -0
- package/dist/lib/tools/write.d.ts.map +1 -0
- package/dist/lib/tools/write.js +44 -0
- package/dist/lib/tools/write.js.map +1 -0
- 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: '
|
|
119
|
-
description:
|
|
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
|
|
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: '
|
|
133
|
-
description:
|
|
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
|
|
282
|
+
description: 'The message to inject when the trigger fires. Should describe what action to take.',
|
|
140
283
|
},
|
|
141
|
-
|
|
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: '
|
|
296
|
+
description: 'The schedule ID to cancel.',
|
|
144
297
|
},
|
|
145
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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: [
|
|
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
|
-
|
|
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({
|
|
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/
|
|
238
|
-
|
|
239
|
-
message,
|
|
240
|
-
|
|
241
|
-
|
|
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 === '
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
|
|
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({
|
|
523
|
+
text: JSON.stringify({ error: 'Missing source_path' }),
|
|
258
524
|
},
|
|
259
525
|
],
|
|
260
526
|
isError: true,
|
|
261
527
|
};
|
|
262
528
|
}
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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,
|