@aiconnect/agentjobs-mcp 1.0.9 → 1.2.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,74 @@
1
+ export class MCPDebugger {
2
+ isDebugEnabled;
3
+ constructor(enabled = process.env.MCP_DEBUG === 'true') {
4
+ this.isDebugEnabled = enabled;
5
+ }
6
+ formatLog(level, message, data) {
7
+ if (!this.isDebugEnabled)
8
+ return;
9
+ const timestamp = new Date().toISOString();
10
+ const prefix = `[MCP-DEBUG ${timestamp}] [${level.toUpperCase()}]`;
11
+ console.error(`${prefix} ${message}`);
12
+ if (data !== undefined) {
13
+ console.error(`${prefix} Data:`, JSON.stringify(data, null, 2));
14
+ }
15
+ }
16
+ info(message, data) {
17
+ this.formatLog('info', message, data);
18
+ }
19
+ warn(message, data) {
20
+ this.formatLog('warn', message, data);
21
+ }
22
+ error(message, data) {
23
+ this.formatLog('error', message, data);
24
+ }
25
+ debug(message, data) {
26
+ this.formatLog('debug', message, data);
27
+ }
28
+ // Helper para debugar chamadas de tools
29
+ toolCall(toolName, args) {
30
+ this.info(`Tool called: ${toolName}`, { args });
31
+ }
32
+ toolResponse(toolName, response, duration) {
33
+ this.info(`Tool response: ${toolName}`, {
34
+ response: typeof response === 'object' ? response : { value: response },
35
+ duration: duration ? `${duration}ms` : undefined
36
+ });
37
+ }
38
+ toolError(toolName, error) {
39
+ this.error(`Tool error: ${toolName}`, {
40
+ error: error.message || error,
41
+ stack: error.stack
42
+ });
43
+ }
44
+ // Helper para debugar requisições HTTP
45
+ httpRequest(method, url, data) {
46
+ this.debug(`HTTP ${method.toUpperCase()} ${url}`, data);
47
+ }
48
+ httpResponse(method, url, status, data) {
49
+ this.debug(`HTTP Response ${method.toUpperCase()} ${url} [${status}]`, data);
50
+ }
51
+ httpError(method, url, error) {
52
+ this.error(`HTTP Error ${method.toUpperCase()} ${url}`, error);
53
+ }
54
+ }
55
+ // Instância global do debugger
56
+ export const mcpDebugger = new MCPDebugger();
57
+ // Helper function para medir tempo de execução
58
+ export function withTiming(fn, name) {
59
+ return new Promise(async (resolve, reject) => {
60
+ const start = Date.now();
61
+ mcpDebugger.debug(`Starting ${name}`);
62
+ try {
63
+ const result = await fn();
64
+ const duration = Date.now() - start;
65
+ mcpDebugger.debug(`Completed ${name} in ${duration}ms`);
66
+ resolve(result);
67
+ }
68
+ catch (error) {
69
+ const duration = Date.now() - start;
70
+ mcpDebugger.error(`Failed ${name} after ${duration}ms`, error);
71
+ reject(error);
72
+ }
73
+ });
74
+ }
@@ -1,4 +1,185 @@
1
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
+ }
2
183
  // Schema para um job individual, baseado no exemplo fornecido.
3
184
  const jobSchema = z.object({
4
185
  job_id: z.string(),
@@ -10,15 +191,6 @@ const jobSchema = z.object({
10
191
  result: z.string().nullable(),
11
192
  job_type_id: z.string(),
12
193
  }).passthrough(); // .passthrough() permite outros campos não definidos no schema.
13
- /**
14
- * Formata a resposta completa de um job, ideal para o get_job.
15
- * @param job - O objeto do job.
16
- * @returns Uma string formatada com os detalhes completos do job.
17
- */
18
- export function formatJobDetails(job) {
19
- const fullJobDetails = JSON.stringify(job, null, 2);
20
- return `Job Details:\n\n${fullJobDetails}`;
21
- }
22
194
  /**
23
195
  * Formata um resumo de um job, com os campos principais.
24
196
  * @param job - O objeto do job.
@@ -37,65 +209,365 @@ export function formatJobSummary(job) {
37
209
  - Result: ${parsedJob.result || 'N/A'}
38
210
  `.trim();
39
211
  }
40
- catch (error) {
212
+ catch {
41
213
  // Se a validação falhar, retorna o objeto como string.
42
214
  return JSON.stringify(job, null, 2);
43
215
  }
44
216
  }
45
217
  /**
46
218
  * Formata a resposta para a lista de jobs.
219
+ *
220
+ * `meta` é obrigatório e os três campos (`count`, `limit`, `total`) precisam
221
+ * ser numéricos — confirmado empiricamente que o backend sempre os retorna em
222
+ * `/services/agent-jobs`. Falha explícita aqui é preferível a inventar um
223
+ * footer com valores inferidos, pois isso recriaria a ambiguidade
224
+ * "page count vs total" que esta tool deveria eliminar.
225
+ *
47
226
  * @param jobs - Um array de jobs.
48
- * @param pagination - O objeto de paginação.
227
+ * @param meta - O objeto `meta` retornado pelo backend.
228
+ * @param offset - O `offset` que a tool enviou na requisição (default 0).
49
229
  * @returns Uma string formatada com a lista de resumos de jobs.
50
230
  */
51
- export function formatJobList(jobs, pagination) {
52
- if (!jobs || jobs.length === 0) {
53
- return "No jobs found for the given criteria.";
231
+ export function formatJobList(jobs, meta, offset = 0) {
232
+ if (!meta || typeof meta.count !== 'number' || typeof meta.limit !== 'number' || typeof meta.total !== 'number') {
233
+ throw new Error('formatJobList: meta is required with numeric `count`, `limit`, and `total`. ' +
234
+ `Received: ${JSON.stringify(meta)}`);
54
235
  }
55
- const jobSummaries = jobs.map(job => formatJobSummary(job)).join('\n\n');
56
- const paginationSummary = `Page: ${Math.floor((pagination.offset || 0) / (pagination.limit || 20)) + 1} | Total Jobs: ${pagination.total}`;
57
- return `Found ${jobs.length} jobs.\n\n${jobSummaries}\n\n${paginationSummary}`;
236
+ const safeJobs = jobs || [];
237
+ const { count, limit, total } = meta;
238
+ const hasMore = (offset + count) < total;
239
+ const nextOffset = hasMore ? offset + limit : null;
240
+ const footer = `Returned: ${count} | Total matching: ${total} | Has more: ${hasMore} | Next offset: ${nextOffset === null ? 'null' : nextOffset}`;
241
+ if (safeJobs.length === 0) {
242
+ return `Found 0 jobs.\n\n${footer}`;
243
+ }
244
+ const jobSummaries = safeJobs.map(job => formatJobSummary(job)).join('\n\n');
245
+ return `Found ${safeJobs.length} jobs.\n\n${jobSummaries}\n\n${footer}`;
58
246
  }
59
247
  // Schema for job type details
60
248
  const jobTypeSchema = z.object({
61
249
  id: z.string(),
62
250
  org_id: z.string(),
63
251
  name: z.string(),
64
- description: z.string(),
252
+ description: z.string().optional(),
65
253
  default_config: z.object({
66
- profile_id: z.string(),
67
- max_follow_ups: z.number(),
68
- max_task_retries: z.number(),
69
- task_retry_interval: z.number().describe("The interval in minutes to wait before retrying a task."),
70
- max_time_to_complete: z.number().describe("The maximum time in minutes to complete a task."),
71
- start_prompt: z.string(),
72
- }),
254
+ profile_id: z.string().optional(),
255
+ max_follow_ups: z.number().optional(),
256
+ max_task_retries: z.number().optional(),
257
+ task_retry_interval: z.number().optional(),
258
+ max_time_to_complete: z.number().optional(),
259
+ failure_cooldown_minutes: z.number().optional(),
260
+ start_prompt: z.string().optional(),
261
+ }).optional(),
262
+ params_schema: z.any().optional(),
263
+ version: z.union([z.string(), z.number()]).optional(),
264
+ visibility: z.string().optional(),
265
+ active: z.boolean().optional(),
266
+ tags: z.union([z.string(), z.array(z.string())]).optional(),
267
+ created_at: isoOrMs,
268
+ updated_at: isoOrMs,
73
269
  }).passthrough();
270
+ // Helper to summarize JSON Schema
271
+ function summarizeSchema(schema, _depth = 1, limit = 12) {
272
+ if (!schema || typeof schema !== 'object') {
273
+ return {
274
+ type: 'unknown',
275
+ requiredCount: 0,
276
+ propsCount: 0,
277
+ properties: []
278
+ };
279
+ }
280
+ const type = schema.type || 'unknown';
281
+ const required = Array.isArray(schema.required) ? schema.required : [];
282
+ const props = schema.properties || {};
283
+ const propNames = Object.keys(props);
284
+ const properties = propNames.slice(0, limit).map(name => ({
285
+ name,
286
+ type: props[name]?.type || 'any',
287
+ required: required.includes(name),
288
+ description: props[name]?.description,
289
+ default: props[name]?.default
290
+ }));
291
+ return {
292
+ type,
293
+ requiredCount: required.length,
294
+ propsCount: propNames.length,
295
+ properties
296
+ };
297
+ }
74
298
  /**
75
299
  * Formats the response for job type details.
76
300
  * @param jobType - The job type object.
301
+ * @param options - Formatter options.
77
302
  * @returns A formatted string with the job type details.
78
303
  */
79
- export function formatJobTypeDetails(jobType) {
304
+ export function formatJobTypeDetails(jobType, options = {}) {
305
+ const { includeSchema = true, schemaDepth = 1, truncate: truncateLimits = {}, renderAsMarkdown = true, showEmptySections = false } = options;
306
+ const { startPrompt: startPromptLimit = 500, description: descriptionLimit = 400, schemaString: schemaStringLimit = 1500 } = truncateLimits;
80
307
  try {
81
- const parsedJobType = jobTypeSchema.parse(jobType);
82
- return `
83
- - ID: ${parsedJobType.id}
84
- - Name: ${parsedJobType.name}
85
- - Description: ${parsedJobType.description}
86
- - Organization ID: ${parsedJobType.org_id}
87
-
88
- Default Configuration:
89
- - Profile ID: ${parsedJobType.default_config.profile_id}
90
- - Max Follow-ups: ${parsedJobType.default_config.max_follow_ups}
91
- - Max Task Retries: ${parsedJobType.default_config.max_task_retries}
92
- - Task Retry Interval: ${parsedJobType.default_config.task_retry_interval} minutes
93
- - Max Time to Complete: ${parsedJobType.default_config.max_time_to_complete} minutes
94
- - Start Prompt: ${parsedJobType.default_config.start_prompt}
95
- `.trim();
308
+ const j = jobTypeSchema.parse(jobType);
309
+ // Normalize dates
310
+ const createdIso = toIso(j.created_at);
311
+ const updatedIso = toIso(j.updated_at);
312
+ // Format tags
313
+ const tagsList = (() => {
314
+ if (!j.tags)
315
+ return [];
316
+ if (typeof j.tags === 'string') {
317
+ return j.tags.split(',').map(s => s.trim()).filter(Boolean);
318
+ }
319
+ return j.tags;
320
+ })();
321
+ // Truncate long texts
322
+ const descriptionText = j.description ? truncate(j.description, descriptionLimit) : undefined;
323
+ const startPromptText = j.default_config?.start_prompt
324
+ ? truncate(j.default_config.start_prompt, startPromptLimit)
325
+ : undefined;
326
+ // Calculate derived fields
327
+ const retryPolicy = j.default_config?.max_task_retries && j.default_config?.task_retry_interval
328
+ ? `${j.default_config.max_task_retries} every ${j.default_config.task_retry_interval} min`
329
+ : undefined;
330
+ const executionWindow = j.default_config?.max_time_to_complete
331
+ ? `${j.default_config.max_time_to_complete} min`
332
+ : undefined;
333
+ const cooldownInfo = j.default_config?.failure_cooldown_minutes
334
+ ? `${j.default_config.failure_cooldown_minutes} min`
335
+ : undefined;
336
+ // Summarize schema
337
+ const schemaSummary = j.params_schema ? summarizeSchema(j.params_schema, schemaDepth) : null;
338
+ const schemaPreview = j.params_schema && includeSchema ? (() => {
339
+ try {
340
+ const text = JSON.stringify(j.params_schema, null, 2);
341
+ return truncate(text, schemaStringLimit);
342
+ }
343
+ catch {
344
+ return 'Unable to serialize schema';
345
+ }
346
+ })() : null;
347
+ // Build policies line
348
+ const policies = [
349
+ retryPolicy ? `retries up to ${retryPolicy}` : null,
350
+ executionWindow ? `window ${executionWindow}` : null,
351
+ cooldownInfo ? `cooldown ${cooldownInfo}` : null
352
+ ].filter(Boolean).join(' | ');
353
+ // Format output
354
+ const title = renderAsMarkdown ? '## Job Type Details\n' : 'Job Type Details\n===========\n';
355
+ let output = title + '\n';
356
+ // Identification
357
+ output += 'Identification:\n';
358
+ output += `- ID: ${j.id}\n`;
359
+ output += `- Name: ${j.name}\n`;
360
+ output += `- Org ID: ${j.org_id}\n`;
361
+ if (j.version !== undefined)
362
+ output += `- Version: ${safe(j.version)}\n`;
363
+ if (j.visibility !== undefined)
364
+ output += `- Visibility: ${safe(j.visibility)}\n`;
365
+ if (j.active !== undefined)
366
+ output += `- Active: ${bool(j.active)}\n`;
367
+ output += '\n';
368
+ // Description
369
+ if (descriptionText || showEmptySections) {
370
+ output += 'Description:\n';
371
+ output += safe(descriptionText) + '\n\n';
372
+ }
373
+ // Default Config
374
+ if (j.default_config || showEmptySections) {
375
+ output += 'Default Config:\n';
376
+ if (j.default_config) {
377
+ const cfg = j.default_config;
378
+ if (cfg.profile_id)
379
+ output += `- Profile ID: ${cfg.profile_id}\n`;
380
+ if (cfg.max_follow_ups !== undefined)
381
+ output += `- Max Follow-ups: ${cfg.max_follow_ups}\n`;
382
+ if (cfg.max_task_retries !== undefined)
383
+ output += `- Max Task Retries: ${cfg.max_task_retries}\n`;
384
+ if (cfg.task_retry_interval !== undefined)
385
+ output += `- Task Retry Interval: ${cfg.task_retry_interval} min\n`;
386
+ if (cfg.max_time_to_complete !== undefined)
387
+ output += `- Max Time to Complete: ${cfg.max_time_to_complete} min\n`;
388
+ if (cfg.failure_cooldown_minutes !== undefined)
389
+ output += `- Failure Cooldown: ${cfg.failure_cooldown_minutes} min\n`;
390
+ if (startPromptText)
391
+ output += `- Start Prompt: ${startPromptText}\n`;
392
+ if (policies)
393
+ output += `- Policies: ${policies}\n`;
394
+ }
395
+ else {
396
+ output += 'n/a\n';
397
+ }
398
+ output += '\n';
399
+ }
400
+ // Params Schema
401
+ if ((schemaSummary && includeSchema) || showEmptySections) {
402
+ output += 'Params Schema:\n';
403
+ if (schemaSummary) {
404
+ output += `- Type: ${schemaSummary.type} | Required: ${schemaSummary.requiredCount} | Properties: ${schemaSummary.propsCount}\n`;
405
+ if (schemaSummary.properties.length > 0) {
406
+ output += '- Properties:\n';
407
+ schemaSummary.properties.forEach(prop => {
408
+ const req = prop.required ? ' (required)' : '';
409
+ const desc = prop.description ? ` — ${truncate(prop.description, 100)}` : '';
410
+ const def = prop.default !== undefined ? ` — Defaults to ${JSON.stringify(prop.default)}` : '';
411
+ output += ` - ${prop.name}: ${prop.type}${req}${desc}${def}\n`;
412
+ });
413
+ if (schemaSummary.propsCount > schemaSummary.properties.length) {
414
+ output += ` - +${schemaSummary.propsCount - schemaSummary.properties.length} more…\n`;
415
+ }
416
+ }
417
+ if (schemaPreview) {
418
+ output += '- Schema preview:\n';
419
+ output += '```json\n' + schemaPreview + '\n```\n';
420
+ }
421
+ }
422
+ else {
423
+ output += 'n/a\n';
424
+ }
425
+ output += '\n';
426
+ }
427
+ // Metadata
428
+ output += 'Metadata:\n';
429
+ output += `- Created At: ${safe(createdIso)}\n`;
430
+ output += `- Updated At: ${safe(updatedIso)}\n`;
431
+ output += `- Tags: ${fmtList(tagsList)}\n`;
432
+ return output.trim();
96
433
  }
97
- catch (error) {
98
- // If validation fails, return the object as a string.
99
- return `Invalid job type details format: ${JSON.stringify(jobType, null, 2)}`;
434
+ catch (e) {
435
+ // If validation fails, return raw JSON
436
+ return `Job Type Details (raw):\n\n${JSON.stringify(jobType, null, 2)}`;
100
437
  }
101
438
  }
439
+ /**
440
+ * Formats a summary of a job type.
441
+ * @param jobType - The job type object.
442
+ * @returns A formatted string with the job type summary.
443
+ */
444
+ export function formatJobTypeSummary(jobType) {
445
+ try {
446
+ const j = jobTypeSchema.parse(jobType);
447
+ const retries = j.default_config?.max_task_retries && j.default_config?.task_retry_interval
448
+ ? `${j.default_config.max_task_retries} every ${j.default_config.task_retry_interval} min`
449
+ : 'n/a';
450
+ const maxTime = j.default_config?.max_time_to_complete
451
+ ? `${j.default_config.max_time_to_complete} min`
452
+ : 'n/a';
453
+ const cooldown = j.default_config?.failure_cooldown_minutes
454
+ ? `${j.default_config.failure_cooldown_minutes} min`
455
+ : 'n/a';
456
+ const schemaSummary = j.params_schema ? summarizeSchema(j.params_schema) : null;
457
+ const schemaInfo = schemaSummary
458
+ ? `required=${schemaSummary.requiredCount}, props=${schemaSummary.propsCount}`
459
+ : 'n/a';
460
+ return [
461
+ `- ID: ${j.id}`,
462
+ `- Name: ${j.name}`,
463
+ `- Active: ${bool(j.active)}`,
464
+ `- Retries: ${retries} | Max Time: ${maxTime} | Cooldown: ${cooldown}`,
465
+ `- Params: ${schemaInfo}`
466
+ ].join('\n');
467
+ }
468
+ catch {
469
+ // If validation fails, return basic info
470
+ return JSON.stringify(jobType, null, 2);
471
+ }
472
+ }
473
+ const DATE_FILTER_KEYS = [
474
+ 'scheduled_at_gte',
475
+ 'scheduled_at_lte',
476
+ 'created_at_gte',
477
+ 'created_at_lte',
478
+ ];
479
+ const renderDateRange = (label, gte, lte) => {
480
+ if (gte === undefined && lte === undefined)
481
+ return null;
482
+ const left = gte !== undefined ? String(gte) : '(open)';
483
+ const right = lte !== undefined ? String(lte) : '(open)';
484
+ return `${label}: ${left} → ${right}`;
485
+ };
486
+ const CONTEXT_LABEL_WIDTH = 16; // "Server version: " == 16 chars
487
+ const JOB_TYPE_ID_WIDTH = 22;
488
+ const JOB_TYPE_EMOJI_WIDTH = 4;
489
+ function padLabel(label) {
490
+ return (label + ':').padEnd(CONTEXT_LABEL_WIDTH, ' ');
491
+ }
492
+ function formatJobTypeLine(jt) {
493
+ const id = jt.id.padEnd(JOB_TYPE_ID_WIDTH, ' ');
494
+ const emoji = (jt.emoji ?? '').padEnd(JOB_TYPE_EMOJI_WIDTH, ' ');
495
+ const description = jt.description ? ` — ${jt.description}` : '';
496
+ return ` - ${id} ${emoji} ${jt.name}${description}`;
497
+ }
498
+ export function formatContext(input) {
499
+ const { localConfig, jobTypes, total, jobTypesError } = input;
500
+ const contextSection = [
501
+ 'Context:',
502
+ ` ${padLabel('Org ID')} ${localConfig.org_id}`,
503
+ ` ${padLabel('Timezone')} ${localConfig.timezone}`,
504
+ ` ${padLabel('API URL')} ${localConfig.api_url}`,
505
+ ` ${padLabel('Server version')} ${localConfig.server_version}`
506
+ ].join('\n');
507
+ let jobsSection;
508
+ if (jobTypesError !== undefined) {
509
+ jobsSection = `Job types: unavailable (error: ${jobTypesError})`;
510
+ }
511
+ else if (jobTypes === undefined) {
512
+ jobsSection = 'Job types: unavailable (error: unknown)';
513
+ }
514
+ else {
515
+ const totalCount = typeof total === 'number' ? total : jobTypes.length;
516
+ if (totalCount === 0) {
517
+ jobsSection = 'Job types available (0):\n (no job types registered for this org)';
518
+ }
519
+ else {
520
+ const lines = jobTypes.map(formatJobTypeLine);
521
+ let trailing = '';
522
+ if (totalCount > jobTypes.length) {
523
+ const missing = totalCount - jobTypes.length;
524
+ trailing = `\n … and ${missing} more job types not shown`;
525
+ }
526
+ jobsSection = `Job types available (${totalCount}):\n${lines.join('\n')}${trailing}`;
527
+ }
528
+ }
529
+ return `${contextSection}\n\n${jobsSection}`;
530
+ }
531
+ export function formatJobStats(stats, appliedFilters = {}) {
532
+ const { waiting = 0, running = 0, completed = 0, failed = 0, canceled = 0, scheduled = 0, } = stats.status;
533
+ const totalJobs = waiting + running + completed + failed + canceled + scheduled;
534
+ const successRate = totalJobs > 0 ? ((completed / (totalJobs - waiting - scheduled - running)) * 100).toFixed(1) : "0.0";
535
+ const completionRate = (completed + failed) > 0 ? (completed / (completed + failed) * 100).toFixed(1) : "0.0";
536
+ const activeJobs = running + waiting + scheduled;
537
+ const filters = appliedFilters || {};
538
+ const scheduledLine = renderDateRange('Scheduled', filters.scheduled_at_gte, filters.scheduled_at_lte);
539
+ const createdLine = renderDateRange('Created', filters.created_at_gte, filters.created_at_lte);
540
+ const hasAnyDateFilter = DATE_FILTER_KEYS.some((k) => filters[k] !== undefined);
541
+ const periodFallback = hasAnyDateFilter ? null : 'Period: All time';
542
+ const dateLines = [scheduledLine, createdLine, periodFallback].filter((l) => l !== null);
543
+ const dateFilterSet = new Set(DATE_FILTER_KEYS);
544
+ const otherFilterEntries = Object.entries(filters).filter(([k, v]) => !dateFilterSet.has(k) && v !== undefined && v !== null && v !== '');
545
+ const filtersSection = otherFilterEntries.length
546
+ ? `Filters:\n${otherFilterEntries.map(([k, v]) => `- ${k}: ${v}`).join('\n')}\n\n`
547
+ : '';
548
+ const percentage = (value) => {
549
+ if (totalJobs === 0)
550
+ return "0.0";
551
+ return ((value / totalJobs) * 100).toFixed(1);
552
+ };
553
+ return `
554
+ Job Statistics Report
555
+ ====================
556
+
557
+ ${dateLines.join('\n')}
558
+
559
+ ${filtersSection}Status Breakdown:
560
+ ✓ Completed: ${completed} jobs (${percentage(completed)}%)
561
+ ⏳ Running: ${running} jobs (${percentage(running)}%)
562
+ ⏰ Scheduled: ${scheduled} jobs (${percentage(scheduled)}%)
563
+ ⏸ Waiting: ${waiting} jobs (${percentage(waiting)}%)
564
+ ✗ Failed: ${failed} jobs (${percentage(failed)}%)
565
+ ⊘ Canceled: ${canceled} jobs (${percentage(canceled)}%)
566
+
567
+ Summary:
568
+ - Total Jobs: ${totalJobs}
569
+ - Success Rate: ${successRate}%
570
+ - Active Jobs: ${activeJobs} (running + waiting + scheduled)
571
+ - Completion Rate: ${completionRate}% (completed / (completed + failed))
572
+ `.trim();
573
+ }