@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.
- package/.env.example +7 -0
- package/README.md +82 -12
- package/build/config.js +8 -3
- package/build/debug.js +86 -0
- package/build/index.js +81 -22
- package/build/lib/agentJobsClient.js +79 -0
- package/build/test-tools.js +53 -0
- package/build/tools/cancel_job.js +58 -0
- package/build/tools/create_job.js +76 -0
- package/build/tools/get_job.js +55 -0
- package/build/tools/get_job_type.js +66 -0
- package/build/tools/get_jobs_stats.js +57 -0
- package/build/tools/list_jobs.js +79 -0
- package/build/utils/debugger.js +74 -0
- package/build/utils/formatters.js +498 -0
- package/build/utils/formatters.test.js +387 -0
- package/build/utils/schemas.js +20 -0
- package/build/utils/schemas.test.js +40 -0
- package/docs/debug-guide.md +203 -0
- package/package.json +12 -4
- package/build/cancel_job.js +0 -71
- package/build/create_job.js +0 -151
- package/build/get_job.js +0 -62
- package/build/list_jobs.js +0 -88
|
@@ -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
|
+
}
|