@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.
- package/.env.example +7 -0
- package/README.md +67 -0
- package/build/config.js +10 -3
- package/build/config.test.js +22 -0
- package/build/debug.js +86 -0
- package/build/index.js +71 -7
- package/build/lib/agentJobsClient.js +90 -0
- package/build/test-tools.js +53 -0
- package/build/tools/cancel_job.js +35 -50
- package/build/tools/create_job.js +49 -69
- package/build/tools/get_context.js +61 -0
- package/build/tools/get_job.js +30 -58
- package/build/tools/get_job_type.js +40 -56
- package/build/tools/get_jobs_stats.js +47 -0
- package/build/tools/list_jobs.js +44 -58
- package/build/utils/debugger.js +74 -0
- package/build/utils/formatters.js +516 -44
- package/build/utils/formatters.test.js +629 -0
- package/build/utils/schemas.js +24 -0
- package/build/utils/schemas.test.js +95 -0
- package/build/utils/version.js +12 -0
- package/build/utils/version.test.js +9 -0
- package/docs/agent-jobs-api.md +41 -0
- package/docs/debug-guide.md +203 -0
- package/package.json +13 -4
|
@@ -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
|
|
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
|
|
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,
|
|
52
|
-
if (!
|
|
53
|
-
|
|
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
|
|
56
|
-
const
|
|
57
|
-
|
|
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().
|
|
70
|
-
max_time_to_complete: z.number().
|
|
71
|
-
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 (
|
|
98
|
-
// If validation fails, return
|
|
99
|
-
return `
|
|
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
|
+
}
|