@aiconnect/agentjobs-mcp 1.0.8 → 1.1.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.
@@ -0,0 +1,498 @@
1
+ import { z } from "zod";
2
+ // Aceita string ISO ou number (timestamp) e converte para string ISO amigável
3
+ const isoOrMs = z.union([z.string(), z.number()]).optional();
4
+ const toIso = (v) => {
5
+ if (v === null || v === undefined)
6
+ return undefined;
7
+ try {
8
+ if (typeof v === 'string') {
9
+ const d = new Date(v);
10
+ return isNaN(d.getTime()) ? v : d.toISOString();
11
+ }
12
+ if (typeof v === 'number') {
13
+ const d = new Date(v);
14
+ return isNaN(d.getTime()) ? String(v) : d.toISOString();
15
+ }
16
+ return String(v);
17
+ }
18
+ catch {
19
+ return String(v);
20
+ }
21
+ };
22
+ const taskSchema = z.object({
23
+ task_id: z.string(),
24
+ created_at: isoOrMs,
25
+ }).passthrough();
26
+ const flagsSchema = z.object({
27
+ is_new_channel: z.boolean().optional(),
28
+ has_human_reply: z.boolean().optional(),
29
+ first_reply_at: isoOrMs.nullable().optional(),
30
+ ignore_cooldown: z.boolean().optional(),
31
+ }).passthrough();
32
+ const channelDataSchema = z.object({
33
+ org_id: z.string().optional(),
34
+ channel_code: z.string().optional(),
35
+ channel_id: z.string().optional(),
36
+ platform: z.string().optional(),
37
+ name: z.string().optional(),
38
+ profile_id: z.string().optional(),
39
+ thread_id: z.string().optional(),
40
+ }).passthrough();
41
+ const jobConfigSchema = z.object({
42
+ profile_id: z.string().optional(),
43
+ max_follow_ups: z.number().optional(),
44
+ max_task_retries: z.number().optional(),
45
+ task_retry_interval: z.number().optional(), // minutos
46
+ max_time_to_complete: z.number().optional(), // minutos
47
+ failure_cooldown_minutes: z.number().optional(),
48
+ start_prompt: z.string().optional(),
49
+ }).passthrough();
50
+ const jobDetailsSchema = z.object({
51
+ job_id: z.string(),
52
+ job_type_id: z.string(),
53
+ org_id: z.string(),
54
+ channel_code: z.string(),
55
+ chat_id: z.string().optional(),
56
+ job_status: z.string(),
57
+ result: z.string().nullable().optional(),
58
+ created_at: isoOrMs,
59
+ updated_at: isoOrMs,
60
+ scheduled_at: isoOrMs.optional(),
61
+ last_task_created_at: isoOrMs.nullable().optional(),
62
+ tags: z.string().optional(),
63
+ execution_log: z.array(z.string()).optional(),
64
+ tasks: z.array(taskSchema).optional().default([]),
65
+ flags: flagsSchema.optional().default({}),
66
+ channel_data: channelDataSchema.optional().default({}),
67
+ job_config: jobConfigSchema.optional().default({}),
68
+ params: z.record(z.any()).optional().default({}),
69
+ }).passthrough();
70
+ const bool = (v) => (v === true ? 'yes' : v === false ? 'no' : 'n/a');
71
+ const safe = (v, fallback = 'n/a') => v === undefined || v === null || v === '' ? fallback : String(v);
72
+ const truncate = (s, max = 300) => {
73
+ if (typeof s !== 'string')
74
+ return s;
75
+ return s.length > max ? `${s.slice(0, max)}…` : s;
76
+ };
77
+ const fmtList = (arr) => (arr && arr.length ? arr.join(', ') : 'n/a');
78
+ export function formatJobDetails(job) {
79
+ try {
80
+ const j = jobDetailsSchema.parse(job);
81
+ // Derivados
82
+ const tasks = j.tasks || [];
83
+ const retriesUsed = Math.max((tasks.length || 0) - 1, 0);
84
+ const maxRetries = j.job_config?.max_task_retries ?? undefined;
85
+ const retriesRemaining = maxRetries !== undefined ? Math.max(maxRetries - retriesUsed, 0) : undefined;
86
+ const createdIso = toIso(j.created_at);
87
+ const updatedIso = toIso(j.updated_at);
88
+ const scheduledIso = toIso(j.scheduled_at);
89
+ const lastTaskIso = toIso(j.last_task_created_at);
90
+ const firstReplyIso = toIso(j.flags?.first_reply_at);
91
+ // Duração aproximada
92
+ let durationLine = 'n/a';
93
+ try {
94
+ const start = scheduledIso ? new Date(scheduledIso).getTime() : createdIso ? new Date(createdIso).getTime() : NaN;
95
+ const end = updatedIso ? new Date(updatedIso).getTime() : Date.now();
96
+ if (!isNaN(start) && !isNaN(end) && end >= start) {
97
+ const ms = end - start;
98
+ const mins = Math.floor(ms / 60000);
99
+ const secs = Math.floor((ms % 60000) / 1000);
100
+ durationLine = `${mins}m ${secs}s`;
101
+ }
102
+ }
103
+ catch {
104
+ // ignore errors
105
+ }
106
+ // Tags em lista
107
+ const tagsList = j.tags ? j.tags.split(',').map(s => s.trim()).filter(Boolean) : [];
108
+ // Tarefas formatadas (mostra as últimas 5)
109
+ const lastTasks = tasks.slice(-5).map((t, i) => ` - [${i + Math.max(tasks.length - 5 + 1, 1)}] ${t.task_id} @ ${safe(toIso(t.created_at))}`);
110
+ // Exec log (últimas 5 linhas)
111
+ const lastLogs = (j.execution_log || []).slice(-5).map((l) => ` - ${l}`);
112
+ // Params (pretty JSON, truncado para visualização)
113
+ const paramsPretty = (() => {
114
+ try {
115
+ const text = JSON.stringify(j.params ?? {}, null, 2);
116
+ return truncate(text, 1500);
117
+ }
118
+ catch {
119
+ return String(j.params);
120
+ }
121
+ })();
122
+ const startPrompt = j.job_config?.start_prompt ? truncate(j.job_config.start_prompt, 500) : undefined;
123
+ return (`Job Details
124
+ ===========
125
+
126
+ Identification:
127
+ - Job ID: ${j.job_id}
128
+ - Status: ${j.job_status}
129
+ - Org ID: ${j.org_id}
130
+ - Channel Code: ${j.channel_code}
131
+ - Chat ID: ${safe(j.chat_id)}
132
+ - Job Type: ${j.job_type_id}
133
+
134
+ Channel:
135
+ - Platform: ${safe(j.channel_data?.platform)}
136
+ - Channel ID: ${safe(j.channel_data?.channel_id)}
137
+ - Name: ${safe(j.channel_data?.name)}
138
+ - Profile ID: ${safe(j.channel_data?.profile_id)}
139
+ - Thread ID: ${safe(j.channel_data?.thread_id)}
140
+
141
+ Type Config:
142
+ - Profile ID: ${safe(j.job_config?.profile_id)}
143
+ - Max Follow-ups: ${safe(j.job_config?.max_follow_ups)}
144
+ - Max Task Retries: ${safe(j.job_config?.max_task_retries)}
145
+ - Task Retry Interval: ${safe(j.job_config?.task_retry_interval)} min
146
+ - Max Time to Complete: ${safe(j.job_config?.max_time_to_complete)} min
147
+ - Failure Cooldown: ${safe(j.job_config?.failure_cooldown_minutes)} min
148
+ - Start Prompt: ${safe(startPrompt)}
149
+
150
+ Flags:
151
+ - is_new_channel: ${bool(j.flags?.is_new_channel)}
152
+ - has_human_reply: ${bool(j.flags?.has_human_reply)}
153
+ - first_reply_at: ${safe(firstReplyIso)}
154
+ - ignore_cooldown: ${bool(j.flags?.ignore_cooldown)}
155
+
156
+ Params:
157
+ ${paramsPretty}
158
+
159
+ Tasks:
160
+ - Total Tasks: ${tasks.length}
161
+ - Retries Used: ${retriesUsed}${maxRetries !== undefined ? ` / ${maxRetries} (remaining: ${retriesRemaining})` : ''}
162
+ ${lastTasks.length ? lastTasks.join('\n') : ' - n/a'}
163
+
164
+ Dates:
165
+ - Created At: ${safe(createdIso)}
166
+ - Updated At: ${safe(updatedIso)}
167
+ - Scheduled At: ${safe(scheduledIso)}
168
+ - Last Task At: ${safe(lastTaskIso)}
169
+ - Duration: ${durationLine}
170
+
171
+ Result / Tags / Log:
172
+ - Result: ${safe(j.result)}
173
+ - Tags: ${fmtList(tagsList)}
174
+ - Execution Log (last 5):
175
+ ${lastLogs.length ? lastLogs.join('\n') : ' - n/a'}
176
+ `).trim();
177
+ }
178
+ catch (e) {
179
+ // Se a validação flexível ainda assim falhar, retorna JSON completo
180
+ return `Job Details (raw):\n\n${JSON.stringify(job, null, 2)}`;
181
+ }
182
+ }
183
+ // Schema para um job individual, baseado no exemplo fornecido.
184
+ const jobSchema = z.object({
185
+ job_id: z.string(),
186
+ channel_code: z.string(),
187
+ created_at: z.string().datetime(),
188
+ updated_at: z.string().datetime(),
189
+ scheduled_at: z.string().datetime(),
190
+ job_status: z.string(),
191
+ result: z.string().nullable(),
192
+ job_type_id: z.string(),
193
+ }).passthrough(); // .passthrough() permite outros campos não definidos no schema.
194
+ /**
195
+ * Formata um resumo de um job, com os campos principais.
196
+ * @param job - O objeto do job.
197
+ * @returns Uma string formatada com o resumo do job.
198
+ */
199
+ export function formatJobSummary(job) {
200
+ try {
201
+ const parsedJob = jobSchema.parse(job);
202
+ return `
203
+ - Job ID: ${parsedJob.job_id}
204
+ - Status: ${parsedJob.job_status}
205
+ - Type: ${parsedJob.job_type_id}
206
+ - Channel: ${parsedJob.channel_code}
207
+ - Scheduled At: ${parsedJob.scheduled_at}
208
+ - Updated At: ${parsedJob.updated_at}
209
+ - Result: ${parsedJob.result || 'N/A'}
210
+ `.trim();
211
+ }
212
+ catch {
213
+ // Se a validação falhar, retorna o objeto como string.
214
+ return JSON.stringify(job, null, 2);
215
+ }
216
+ }
217
+ /**
218
+ * Formata a resposta para a lista de jobs.
219
+ * @param jobs - Um array de jobs.
220
+ * @param pagination - O objeto de paginação.
221
+ * @returns Uma string formatada com a lista de resumos de jobs.
222
+ */
223
+ export function formatJobList(jobs, pagination) {
224
+ if (!jobs || jobs.length === 0) {
225
+ return "No jobs found for the given criteria.";
226
+ }
227
+ const jobSummaries = jobs.map(job => formatJobSummary(job)).join('\n\n');
228
+ const paginationSummary = `Page: ${Math.floor((pagination.offset || 0) / (pagination.limit || 20)) + 1} | Total Jobs: ${pagination.total}`;
229
+ return `Found ${jobs.length} jobs.\n\n${jobSummaries}\n\n${paginationSummary}`;
230
+ }
231
+ // Schema for job type details
232
+ const jobTypeSchema = z.object({
233
+ id: z.string(),
234
+ org_id: z.string(),
235
+ name: z.string(),
236
+ description: z.string().optional(),
237
+ default_config: z.object({
238
+ profile_id: z.string().optional(),
239
+ max_follow_ups: z.number().optional(),
240
+ max_task_retries: z.number().optional(),
241
+ task_retry_interval: z.number().optional(),
242
+ max_time_to_complete: z.number().optional(),
243
+ failure_cooldown_minutes: z.number().optional(),
244
+ start_prompt: z.string().optional(),
245
+ }).optional(),
246
+ params_schema: z.any().optional(),
247
+ version: z.union([z.string(), z.number()]).optional(),
248
+ visibility: z.string().optional(),
249
+ active: z.boolean().optional(),
250
+ tags: z.union([z.string(), z.array(z.string())]).optional(),
251
+ created_at: isoOrMs,
252
+ updated_at: isoOrMs,
253
+ }).passthrough();
254
+ // Helper to summarize JSON Schema
255
+ function summarizeSchema(schema, _depth = 1, limit = 12) {
256
+ if (!schema || typeof schema !== 'object') {
257
+ return {
258
+ type: 'unknown',
259
+ requiredCount: 0,
260
+ propsCount: 0,
261
+ properties: []
262
+ };
263
+ }
264
+ const type = schema.type || 'unknown';
265
+ const required = Array.isArray(schema.required) ? schema.required : [];
266
+ const props = schema.properties || {};
267
+ const propNames = Object.keys(props);
268
+ const properties = propNames.slice(0, limit).map(name => ({
269
+ name,
270
+ type: props[name]?.type || 'any',
271
+ required: required.includes(name),
272
+ description: props[name]?.description,
273
+ default: props[name]?.default
274
+ }));
275
+ return {
276
+ type,
277
+ requiredCount: required.length,
278
+ propsCount: propNames.length,
279
+ properties
280
+ };
281
+ }
282
+ /**
283
+ * Formats the response for job type details.
284
+ * @param jobType - The job type object.
285
+ * @param options - Formatter options.
286
+ * @returns A formatted string with the job type details.
287
+ */
288
+ export function formatJobTypeDetails(jobType, options = {}) {
289
+ const { includeSchema = true, schemaDepth = 1, truncate: truncateLimits = {}, renderAsMarkdown = true, showEmptySections = false } = options;
290
+ const { startPrompt: startPromptLimit = 500, description: descriptionLimit = 400, schemaString: schemaStringLimit = 1500 } = truncateLimits;
291
+ try {
292
+ const j = jobTypeSchema.parse(jobType);
293
+ // Normalize dates
294
+ const createdIso = toIso(j.created_at);
295
+ const updatedIso = toIso(j.updated_at);
296
+ // Format tags
297
+ const tagsList = (() => {
298
+ if (!j.tags)
299
+ return [];
300
+ if (typeof j.tags === 'string') {
301
+ return j.tags.split(',').map(s => s.trim()).filter(Boolean);
302
+ }
303
+ return j.tags;
304
+ })();
305
+ // Truncate long texts
306
+ const descriptionText = j.description ? truncate(j.description, descriptionLimit) : undefined;
307
+ const startPromptText = j.default_config?.start_prompt
308
+ ? truncate(j.default_config.start_prompt, startPromptLimit)
309
+ : undefined;
310
+ // Calculate derived fields
311
+ const retryPolicy = j.default_config?.max_task_retries && j.default_config?.task_retry_interval
312
+ ? `${j.default_config.max_task_retries} every ${j.default_config.task_retry_interval} min`
313
+ : undefined;
314
+ const executionWindow = j.default_config?.max_time_to_complete
315
+ ? `${j.default_config.max_time_to_complete} min`
316
+ : undefined;
317
+ const cooldownInfo = j.default_config?.failure_cooldown_minutes
318
+ ? `${j.default_config.failure_cooldown_minutes} min`
319
+ : undefined;
320
+ // Summarize schema
321
+ const schemaSummary = j.params_schema ? summarizeSchema(j.params_schema, schemaDepth) : null;
322
+ const schemaPreview = j.params_schema && includeSchema ? (() => {
323
+ try {
324
+ const text = JSON.stringify(j.params_schema, null, 2);
325
+ return truncate(text, schemaStringLimit);
326
+ }
327
+ catch {
328
+ return 'Unable to serialize schema';
329
+ }
330
+ })() : null;
331
+ // Build policies line
332
+ const policies = [
333
+ retryPolicy ? `retries up to ${retryPolicy}` : null,
334
+ executionWindow ? `window ${executionWindow}` : null,
335
+ cooldownInfo ? `cooldown ${cooldownInfo}` : null
336
+ ].filter(Boolean).join(' | ');
337
+ // Format output
338
+ const title = renderAsMarkdown ? '## Job Type Details\n' : 'Job Type Details\n===========\n';
339
+ let output = title + '\n';
340
+ // Identification
341
+ output += 'Identification:\n';
342
+ output += `- ID: ${j.id}\n`;
343
+ output += `- Name: ${j.name}\n`;
344
+ output += `- Org ID: ${j.org_id}\n`;
345
+ if (j.version !== undefined)
346
+ output += `- Version: ${safe(j.version)}\n`;
347
+ if (j.visibility !== undefined)
348
+ output += `- Visibility: ${safe(j.visibility)}\n`;
349
+ if (j.active !== undefined)
350
+ output += `- Active: ${bool(j.active)}\n`;
351
+ output += '\n';
352
+ // Description
353
+ if (descriptionText || showEmptySections) {
354
+ output += 'Description:\n';
355
+ output += safe(descriptionText) + '\n\n';
356
+ }
357
+ // Default Config
358
+ if (j.default_config || showEmptySections) {
359
+ output += 'Default Config:\n';
360
+ if (j.default_config) {
361
+ const cfg = j.default_config;
362
+ if (cfg.profile_id)
363
+ output += `- Profile ID: ${cfg.profile_id}\n`;
364
+ if (cfg.max_follow_ups !== undefined)
365
+ output += `- Max Follow-ups: ${cfg.max_follow_ups}\n`;
366
+ if (cfg.max_task_retries !== undefined)
367
+ output += `- Max Task Retries: ${cfg.max_task_retries}\n`;
368
+ if (cfg.task_retry_interval !== undefined)
369
+ output += `- Task Retry Interval: ${cfg.task_retry_interval} min\n`;
370
+ if (cfg.max_time_to_complete !== undefined)
371
+ output += `- Max Time to Complete: ${cfg.max_time_to_complete} min\n`;
372
+ if (cfg.failure_cooldown_minutes !== undefined)
373
+ output += `- Failure Cooldown: ${cfg.failure_cooldown_minutes} min\n`;
374
+ if (startPromptText)
375
+ output += `- Start Prompt: ${startPromptText}\n`;
376
+ if (policies)
377
+ output += `- Policies: ${policies}\n`;
378
+ }
379
+ else {
380
+ output += 'n/a\n';
381
+ }
382
+ output += '\n';
383
+ }
384
+ // Params Schema
385
+ if ((schemaSummary && includeSchema) || showEmptySections) {
386
+ output += 'Params Schema:\n';
387
+ if (schemaSummary) {
388
+ output += `- Type: ${schemaSummary.type} | Required: ${schemaSummary.requiredCount} | Properties: ${schemaSummary.propsCount}\n`;
389
+ if (schemaSummary.properties.length > 0) {
390
+ output += '- Properties:\n';
391
+ schemaSummary.properties.forEach(prop => {
392
+ const req = prop.required ? ' (required)' : '';
393
+ const desc = prop.description ? ` — ${truncate(prop.description, 100)}` : '';
394
+ const def = prop.default !== undefined ? ` — Defaults to ${JSON.stringify(prop.default)}` : '';
395
+ output += ` - ${prop.name}: ${prop.type}${req}${desc}${def}\n`;
396
+ });
397
+ if (schemaSummary.propsCount > schemaSummary.properties.length) {
398
+ output += ` - +${schemaSummary.propsCount - schemaSummary.properties.length} more…\n`;
399
+ }
400
+ }
401
+ if (schemaPreview) {
402
+ output += '- Schema preview:\n';
403
+ output += '```json\n' + schemaPreview + '\n```\n';
404
+ }
405
+ }
406
+ else {
407
+ output += 'n/a\n';
408
+ }
409
+ output += '\n';
410
+ }
411
+ // Metadata
412
+ output += 'Metadata:\n';
413
+ output += `- Created At: ${safe(createdIso)}\n`;
414
+ output += `- Updated At: ${safe(updatedIso)}\n`;
415
+ output += `- Tags: ${fmtList(tagsList)}\n`;
416
+ return output.trim();
417
+ }
418
+ catch (e) {
419
+ // If validation fails, return raw JSON
420
+ return `Job Type Details (raw):\n\n${JSON.stringify(jobType, null, 2)}`;
421
+ }
422
+ }
423
+ /**
424
+ * Formats a summary of a job type.
425
+ * @param jobType - The job type object.
426
+ * @returns A formatted string with the job type summary.
427
+ */
428
+ export function formatJobTypeSummary(jobType) {
429
+ try {
430
+ const j = jobTypeSchema.parse(jobType);
431
+ const retries = j.default_config?.max_task_retries && j.default_config?.task_retry_interval
432
+ ? `${j.default_config.max_task_retries} every ${j.default_config.task_retry_interval} min`
433
+ : 'n/a';
434
+ const maxTime = j.default_config?.max_time_to_complete
435
+ ? `${j.default_config.max_time_to_complete} min`
436
+ : 'n/a';
437
+ const cooldown = j.default_config?.failure_cooldown_minutes
438
+ ? `${j.default_config.failure_cooldown_minutes} min`
439
+ : 'n/a';
440
+ const schemaSummary = j.params_schema ? summarizeSchema(j.params_schema) : null;
441
+ const schemaInfo = schemaSummary
442
+ ? `required=${schemaSummary.requiredCount}, props=${schemaSummary.propsCount}`
443
+ : 'n/a';
444
+ return [
445
+ `- ID: ${j.id}`,
446
+ `- Name: ${j.name}`,
447
+ `- Active: ${bool(j.active)}`,
448
+ `- Retries: ${retries} | Max Time: ${maxTime} | Cooldown: ${cooldown}`,
449
+ `- Params: ${schemaInfo}`
450
+ ].join('\n');
451
+ }
452
+ catch {
453
+ // If validation fails, return basic info
454
+ return JSON.stringify(jobType, null, 2);
455
+ }
456
+ }
457
+ export function formatJobStats(stats, filters) {
458
+ const { waiting = 0, running = 0, completed = 0, failed = 0, canceled = 0, scheduled = 0, } = stats.status;
459
+ const totalJobs = waiting + running + completed + failed + canceled + scheduled;
460
+ const successRate = totalJobs > 0 ? ((completed / (totalJobs - waiting - scheduled - running)) * 100).toFixed(1) : "0.0";
461
+ const completionRate = (completed + failed) > 0 ? (completed / (completed + failed) * 100).toFixed(1) : "0.0";
462
+ const activeJobs = running + waiting + scheduled;
463
+ let period = "All time";
464
+ if (filters) {
465
+ if (filters.scheduled_at_gte || filters.scheduled_at_lte) {
466
+ const startDate = filters.scheduled_at_gte ? new Date(filters.scheduled_at_gte).toLocaleDateString() : "";
467
+ const endDate = filters.scheduled_at_lte ? new Date(filters.scheduled_at_lte).toLocaleDateString() : "";
468
+ period = `${startDate} to ${endDate}`;
469
+ }
470
+ }
471
+ const org = filters?.org_id ? `Organization: ${filters.org_id}` : "";
472
+ const percentage = (value) => {
473
+ if (totalJobs === 0)
474
+ return "0.0";
475
+ return ((value / totalJobs) * 100).toFixed(1);
476
+ };
477
+ return `
478
+ Job Statistics Report
479
+ ====================
480
+
481
+ Period: ${period}
482
+ ${org}
483
+
484
+ Status Breakdown:
485
+ ✓ Completed: ${completed} jobs (${percentage(completed)}%)
486
+ ⏳ Running: ${running} jobs (${percentage(running)}%)
487
+ ⏰ Scheduled: ${scheduled} jobs (${percentage(scheduled)}%)
488
+ ⏸ Waiting: ${waiting} jobs (${percentage(waiting)}%)
489
+ ✗ Failed: ${failed} jobs (${percentage(failed)}%)
490
+ ⊘ Canceled: ${canceled} jobs (${percentage(canceled)}%)
491
+
492
+ Summary:
493
+ - Total Jobs: ${totalJobs}
494
+ - Success Rate: ${successRate}%
495
+ - Active Jobs: ${activeJobs} (running + waiting + scheduled)
496
+ - Completion Rate: ${completionRate}% (completed / (completed + failed))
497
+ `.trim();
498
+ }