@goodandready/dsh-cron 0.1.24 → 0.2.1
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/README.md +117 -82
- package/docs/README.ru.md +133 -85
- package/docs/README.zh.md +140 -82
- package/docs/design/DESIGN.md +46 -37
- package/lib/chat-start.js +2 -2
- package/lib/client.js +709 -424
- package/lib/http-utils.js +81 -0
- package/lib/index.js +379 -389
- package/lib/integrations.js +0 -1
- package/lib/prompt.js +42 -40
- package/lib/runner.js +48 -34
- package/lib/scheduler.js +33 -32
- package/lib/store.js +1 -1
- package/lib/telegram.js +27 -13
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -8,126 +8,131 @@ import { createKanbanCard } from './integrations.js';
|
|
|
8
8
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
9
9
|
import { randomUUID } from 'node:crypto';
|
|
10
10
|
import z from '@deepseek-ai/schemastery';
|
|
11
|
+
import {
|
|
12
|
+
parseJsonBody,
|
|
13
|
+
isCrossOrigin,
|
|
14
|
+
pickPatchableFields,
|
|
15
|
+
SCRIPT_CONFIRM_HEADER,
|
|
16
|
+
} from './http-utils.js';
|
|
11
17
|
|
|
12
18
|
export const name = '@goodandready/dsh-cron';
|
|
13
19
|
export const inject = ['tools', 'webServer', 'settings', 'llm', 'agents', 'agentDefaultModel'];
|
|
14
20
|
|
|
15
21
|
export const Config = z.object({
|
|
16
|
-
botToken: z.string().role('secret').default('').description('Telegram Bot
|
|
17
|
-
chatId: z.string().default('').description('Telegram
|
|
18
|
-
notifyTelegram: z.boolean().default(false).description('
|
|
19
|
-
onlyOnFailure: z.boolean().default(false).description('
|
|
20
|
-
kanbanBaseUrl: z.string().default('http://127.0.0.1:3000').description('
|
|
22
|
+
botToken: z.string().role('secret').default('').description('Telegram Bot API token used to deliver task reports'),
|
|
23
|
+
chatId: z.string().default('').description('Telegram chat ID that receives task reports'),
|
|
24
|
+
notifyTelegram: z.boolean().default(false).description('Deliver task reports to Telegram for every task'),
|
|
25
|
+
onlyOnFailure: z.boolean().default(false).description('Deliver reports only for failed runs'),
|
|
26
|
+
kanbanBaseUrl: z.string().default('http://127.0.0.1:3000').description('Base URL of the dsh-kanban HTTP API'),
|
|
21
27
|
});
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
const PATCHABLE_TASK_FIELDS = [
|
|
30
|
+
'title',
|
|
31
|
+
'schedule',
|
|
32
|
+
'scheduleText',
|
|
33
|
+
'prompt',
|
|
34
|
+
'type',
|
|
35
|
+
'delivery',
|
|
36
|
+
'provider',
|
|
37
|
+
'model',
|
|
38
|
+
'notifyTelegram',
|
|
39
|
+
'onlyOnFailure',
|
|
40
|
+
'timeoutSeconds',
|
|
41
|
+
'overlapPolicy',
|
|
42
|
+
'kanbanMode',
|
|
43
|
+
'status',
|
|
44
|
+
'oneShot',
|
|
45
|
+
];
|
|
37
46
|
|
|
38
47
|
function sendJson(res, statusCode, data) {
|
|
39
48
|
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
40
49
|
res.end(JSON.stringify(data));
|
|
41
50
|
}
|
|
42
51
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
function rejectCrossOrigin(req, res) {
|
|
53
|
+
if (isCrossOrigin(req)) {
|
|
54
|
+
sendJson(res, 403, { ok: false, error: 'Cross-origin request rejected' });
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
async function readBody(req, res) {
|
|
61
|
+
try {
|
|
62
|
+
return { body: await parseJsonBody(req) };
|
|
63
|
+
} catch (err) {
|
|
64
|
+
sendJson(res, err.statusCode || 400, { ok: false, error: err.message });
|
|
65
|
+
return { error: true };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Apply settings writes through the registered settings scope so the core
|
|
71
|
+
* snapshot stays authoritative (#102). Returns null when the settings
|
|
72
|
+
* service is unavailable (headless profiles fall back to store-only writes).
|
|
73
|
+
*/
|
|
74
|
+
async function applySettingsToScope(settingsScope, body) {
|
|
75
|
+
if (!settingsScope || typeof settingsScope.set !== 'function') return null;
|
|
76
|
+
const errors = [];
|
|
77
|
+
for (const key of ['botToken', 'chatId', 'notifyTelegram', 'onlyOnFailure', 'kanbanBaseUrl']) {
|
|
78
|
+
if (!Object.prototype.hasOwnProperty.call(body, key)) continue;
|
|
50
79
|
try {
|
|
51
|
-
|
|
52
|
-
base: store.getSettings()
|
|
53
|
-
});
|
|
54
|
-
if (scope && typeof scope.watch === 'function') {
|
|
55
|
-
scope.watch((updated) => {
|
|
56
|
-
if (updated && typeof updated === 'object') {
|
|
57
|
-
store.saveSettings(updated);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
}
|
|
80
|
+
await settingsScope.set(key, body[key]);
|
|
61
81
|
} catch (e) {
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
scheduler.start();
|
|
67
|
-
|
|
68
|
-
ctx.effect(() => {
|
|
69
|
-
return () => {
|
|
70
|
-
scheduler.stopAll();
|
|
71
|
-
};
|
|
72
|
-
}, 'dsh-cron: lifecycle');
|
|
73
|
-
|
|
74
|
-
const recommendations = [
|
|
75
|
-
{
|
|
76
|
-
id: 'rec_daily_digest',
|
|
77
|
-
title: 'Ежедневная сводка',
|
|
78
|
-
schedule: '0 8 * * 1-5',
|
|
79
|
-
scheduleText: 'В будние дни в 8:00',
|
|
80
|
-
prompt: 'Сделай краткую утреннюю сводку: проверь список активных задач, открытые тикеты в Gitea и наметь ключевые приоритеты на сегодня.',
|
|
81
|
-
description: 'Начинайте каждый будний день со сводки календаря, непрочитанных писем и приоритетов',
|
|
82
|
-
icon: 'bell'
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
id: 'rec_weekly_review',
|
|
86
|
-
title: 'Еженедельный обзор',
|
|
87
|
-
schedule: '0 16 * * 5',
|
|
88
|
-
scheduleText: 'Пятницы в 16:00',
|
|
89
|
-
prompt: 'Подготовь еженедельный отчет о проделанной работе: список закрытых задач, созданных PR и актуальный статус текущих проектов.',
|
|
90
|
-
description: 'Каждую пятницу создавайте краткий отчет о проделанной работе',
|
|
91
|
-
icon: 'clipboard'
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
id: 'rec_monitor_status',
|
|
95
|
-
title: 'Мониторинг дальнейших действий',
|
|
96
|
-
schedule: '0 9 * * 1-5',
|
|
97
|
-
scheduleText: 'В будние дни в 9:00',
|
|
98
|
-
prompt: 'Проверь недавнюю активность в проектах, статус сборок и отметь всё, что требует внимания разработчика.',
|
|
99
|
-
description: 'Проверяйте недавнюю активность в проектах и отмечайте всё, что требует вашего внимания',
|
|
100
|
-
icon: 'activity'
|
|
82
|
+
errors.push(key + ': ' + ((e && e.message) || e));
|
|
101
83
|
}
|
|
102
|
-
|
|
84
|
+
}
|
|
85
|
+
return errors;
|
|
86
|
+
}
|
|
103
87
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Shared implementation for the /dsh-cron/tasks... and /dsh-cron/action...
|
|
90
|
+
* route families — the #66 alias is kept alive by the same code path instead
|
|
91
|
+
* of a duplicated handler.
|
|
92
|
+
*
|
|
93
|
+
* IMPORTANT: prefix routes must be registered WITHOUT a trailing slash —
|
|
94
|
+
* with one, the core degrades matching to exact-path equality and every
|
|
95
|
+
* deeper path unrouted (#96).
|
|
96
|
+
*/
|
|
97
|
+
function createCronApiHandler(store, scheduler, collection) {
|
|
98
|
+
return async function handleCronApi(req, res) {
|
|
99
|
+
try {
|
|
100
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
101
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
102
|
+
const scope = parts[1]; // 'tasks' | 'action'
|
|
103
|
+
const id = parts[2];
|
|
104
|
+
const action = parts[3];
|
|
105
|
+
|
|
106
|
+
// Collection level: /dsh-cron/tasks (list + create)
|
|
107
|
+
if (!id) {
|
|
108
|
+
if (scope === 'action' || !collection) {
|
|
109
|
+
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
111
112
|
if (req.method === 'GET') {
|
|
112
|
-
const url = new URL(req.url, 'http://127.0.0.1');
|
|
113
113
|
const status = url.searchParams.get('status') || 'all';
|
|
114
114
|
const query = url.searchParams.get('query') || '';
|
|
115
115
|
const list = store.list({ status, query });
|
|
116
116
|
const stats = store.getAggregatedStats();
|
|
117
|
-
sendJson(res, 200, { ok: true, tasks: list, recommendations, stats });
|
|
117
|
+
sendJson(res, 200, { ok: true, tasks: list, recommendations: collection.recommendations, stats });
|
|
118
118
|
return;
|
|
119
119
|
}
|
|
120
|
-
|
|
121
120
|
if (req.method === 'POST') {
|
|
122
|
-
|
|
121
|
+
if (rejectCrossOrigin(req, res)) return;
|
|
122
|
+
const { body, error } = await readBody(req, res);
|
|
123
|
+
if (error) return;
|
|
123
124
|
if (!body.title || !body.schedule || !body.prompt) {
|
|
124
|
-
sendJson(res, 400, { ok: false, error: '
|
|
125
|
+
sendJson(res, 400, { ok: false, error: 'Fields title, schedule and prompt are required' });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const taskType = body.type === 'script' ? 'script' : 'llm';
|
|
129
|
+
if (taskType === 'script' && req.headers[SCRIPT_CONFIRM_HEADER] !== 'script') {
|
|
130
|
+
sendJson(res, 403, { ok: false, error: 'Creating script tasks over HTTP requires the x-dsh-cron-confirm: script header' });
|
|
125
131
|
return;
|
|
126
132
|
}
|
|
127
133
|
const parsed = parseScheduleExpression(body.schedule);
|
|
128
134
|
const current = body.id ? store.get(body.id) : null;
|
|
129
135
|
const resolvedStatus = body.status || (current ? current.status : 'active');
|
|
130
|
-
const taskType = body.type === 'script' ? 'script' : 'llm';
|
|
131
136
|
|
|
132
137
|
const task = store.set({
|
|
133
138
|
id: body.id,
|
|
@@ -155,13 +160,242 @@ export function apply(ctx, config) {
|
|
|
155
160
|
sendJson(res, 200, { ok: true, task });
|
|
156
161
|
return;
|
|
157
162
|
}
|
|
158
|
-
|
|
159
163
|
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
160
|
-
|
|
161
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
164
|
+
return;
|
|
162
165
|
}
|
|
166
|
+
|
|
167
|
+
// Item level: /dsh-cron/{tasks|action}/:id[/:action]
|
|
168
|
+
if (req.method !== 'GET' && rejectCrossOrigin(req, res)) return;
|
|
169
|
+
|
|
170
|
+
if (req.method === 'GET' && (action === 'history' || !action)) {
|
|
171
|
+
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
172
|
+
sendJson(res, 200, { ok: true, history: store.getHistory(id, limit) });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (req.method === 'POST' && action === 'run') {
|
|
177
|
+
await scheduler.triggerManualRun(id);
|
|
178
|
+
sendJson(res, 200, { ok: true, task: store.get(id) });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (req.method === 'POST' && action === 'pause') {
|
|
183
|
+
const task = scheduler.pauseTask(id);
|
|
184
|
+
if (!task) {
|
|
185
|
+
sendJson(res, 404, { ok: false, error: 'Task not found' });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
sendJson(res, 200, { ok: true, task });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (req.method === 'POST' && action === 'resume') {
|
|
193
|
+
const task = scheduler.resumeTask(id);
|
|
194
|
+
if (!task) {
|
|
195
|
+
sendJson(res, 404, { ok: false, error: 'Task not found' });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
sendJson(res, 200, { ok: true, task });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (req.method === 'POST' && (action === 'toggle' || (!action && url.searchParams.get('action') === 'toggle'))) {
|
|
203
|
+
const current = store.get(id);
|
|
204
|
+
if (!current) {
|
|
205
|
+
sendJson(res, 404, { ok: false, error: 'Task not found' });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const task = scheduler.toggleTask(id);
|
|
209
|
+
sendJson(res, 200, { ok: true, task });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (req.method === 'PATCH') {
|
|
214
|
+
const current = store.get(id);
|
|
215
|
+
if (!current) {
|
|
216
|
+
sendJson(res, 404, { ok: false, error: 'Task not found' });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const { body, error } = await readBody(req, res);
|
|
220
|
+
if (error) return;
|
|
221
|
+
// Whitelist patchable fields so stats/timestamps stay server-owned (#90)
|
|
222
|
+
const patchData = pickPatchableFields(body, PATCHABLE_TASK_FIELDS);
|
|
223
|
+
if (patchData.type === 'script' && current.type !== 'script' && req.headers[SCRIPT_CONFIRM_HEADER] !== 'script') {
|
|
224
|
+
sendJson(res, 403, { ok: false, error: 'Switching a task to the script type over HTTP requires the x-dsh-cron-confirm: script header' });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (patchData.schedule) {
|
|
228
|
+
const parsed = parseScheduleExpression(patchData.schedule);
|
|
229
|
+
patchData.schedule = parsed.cronPattern || patchData.schedule;
|
|
230
|
+
patchData.scheduleText = body.scheduleText || parsed.humanText;
|
|
231
|
+
patchData.oneShot = Boolean(parsed.isOneShot || body.oneShot);
|
|
232
|
+
}
|
|
233
|
+
const task = store.set({ ...current, ...patchData, id });
|
|
234
|
+
if (task.status === 'active') {
|
|
235
|
+
scheduler.scheduleTask(task);
|
|
236
|
+
} else {
|
|
237
|
+
scheduler.pauseTask(id);
|
|
238
|
+
}
|
|
239
|
+
sendJson(res, 200, { ok: true, task });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (req.method === 'DELETE' || (req.method === 'POST' && action === 'delete')) {
|
|
244
|
+
const current = store.get(id);
|
|
245
|
+
if (current) {
|
|
246
|
+
scheduler.pauseTask(id);
|
|
247
|
+
store.delete(id);
|
|
248
|
+
}
|
|
249
|
+
sendJson(res, 200, { ok: true });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
254
|
+
} catch (err) {
|
|
255
|
+
sendJson(res, err.statusCode || 500, { ok: false, error: err.message });
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Single implementation behind the cron_create_task / cron_schedule_task
|
|
262
|
+
* alias pair (#70, #92).
|
|
263
|
+
*/
|
|
264
|
+
function executeCreateTask(store, scheduler, args) {
|
|
265
|
+
const taskType = args.type === 'script' ? 'script' : 'llm';
|
|
266
|
+
const parsed = parseScheduleExpression(args.schedule);
|
|
267
|
+
const task = store.set({
|
|
268
|
+
title: args.title,
|
|
269
|
+
schedule: parsed.cronPattern || args.schedule,
|
|
270
|
+
scheduleText: parsed.humanText,
|
|
271
|
+
prompt: args.prompt,
|
|
272
|
+
type: taskType,
|
|
273
|
+
delivery: args.delivery || 'isolated',
|
|
274
|
+
status: 'active',
|
|
275
|
+
provider: args.provider,
|
|
276
|
+
model: args.model,
|
|
277
|
+
notifyTelegram: Boolean(args.notifyTelegram),
|
|
278
|
+
onlyOnFailure: Boolean(args.onlyOnFailure),
|
|
279
|
+
timeoutSeconds: Number(args.timeoutSeconds) || 1800,
|
|
280
|
+
overlapPolicy: args.overlapPolicy || 'skip',
|
|
281
|
+
kanbanMode: args.kanbanMode || 'none',
|
|
282
|
+
oneShot: Boolean(parsed.isOneShot),
|
|
283
|
+
});
|
|
284
|
+
scheduler.scheduleTask(task);
|
|
285
|
+
return {
|
|
286
|
+
success: true,
|
|
287
|
+
message: `Task "${task.title}" scheduled successfully (${task.scheduleText})`,
|
|
288
|
+
task
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const createTaskParameters = {
|
|
293
|
+
title: { type: 'string', description: 'Short task name', required: true },
|
|
294
|
+
schedule: { type: 'string', description: 'Schedule: cron ("0 9 * * *"), interval ("every 2h") or one-shot ("in 30m", "at: 2026-09-05T15:00:00Z")', required: true },
|
|
295
|
+
prompt: { type: 'string', description: 'Prompt/instruction for the agent at run time', required: true },
|
|
296
|
+
type: { type: 'string', enum: ['prompt', 'script'], description: 'Task type' },
|
|
297
|
+
delivery: { type: 'string', enum: ['current', 'isolated'], description: 'Run mode: current (in the current chat) or isolated (separate session)' },
|
|
298
|
+
provider: { type: 'string', description: 'Model provider (optional)' },
|
|
299
|
+
model: { type: 'string', description: 'Model (optional)' },
|
|
300
|
+
notifyTelegram: { type: 'boolean', description: 'Send the run report to Telegram' },
|
|
301
|
+
onlyOnFailure: { type: 'boolean', description: 'Send reports only for failures' },
|
|
302
|
+
timeoutSeconds: { type: 'number', description: 'Execution time limit in seconds' },
|
|
303
|
+
overlapPolicy: { type: 'string', enum: ['skip', 'queue', 'replace'], description: 'Overlap policy' },
|
|
304
|
+
kanbanMode: { type: 'string', enum: ['none', 'on_failure', 'always'], description: 'Create a dsh-kanban card for runs' },
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const createTaskOutput = {
|
|
308
|
+
schema: {
|
|
309
|
+
type: 'object',
|
|
310
|
+
additionalProperties: true,
|
|
311
|
+
properties: {
|
|
312
|
+
success: { type: 'boolean' },
|
|
313
|
+
message: { type: 'string' },
|
|
314
|
+
task: { type: 'object', additionalProperties: true }
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
render: (_args, val) => [{ type: 'text', text: val.message || JSON.stringify(val) }]
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
export function apply(ctx, config) {
|
|
321
|
+
const store = new TaskStore();
|
|
322
|
+
const runner = new SessionRunner(ctx);
|
|
323
|
+
const scheduler = new TaskScheduler(store, (task, opts) => runner.execute(task, opts));
|
|
324
|
+
|
|
325
|
+
// Register settings in the Harness settings service (#71, #102): the scope
|
|
326
|
+
// is the source of truth for the core, scope.watch mirrors values into the
|
|
327
|
+
// plugin store, and REST writes go through scope.set so nothing bypasses
|
|
328
|
+
// the settings service.
|
|
329
|
+
let cronSettingsScope = null;
|
|
330
|
+
ctx.inject(['settings'], (sctx) => {
|
|
331
|
+
try {
|
|
332
|
+
cronSettingsScope = sctx.settings.register('dsh-cron', Config, {
|
|
333
|
+
base: store.getSettings()
|
|
334
|
+
});
|
|
335
|
+
if (cronSettingsScope && typeof cronSettingsScope.watch === 'function') {
|
|
336
|
+
cronSettingsScope.watch((updated) => {
|
|
337
|
+
if (updated && typeof updated === 'object') {
|
|
338
|
+
store.saveSettings(updated);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
} catch (e) {
|
|
343
|
+
console.warn('[dsh-cron] settings registration warning:', e.message);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
scheduler.start();
|
|
348
|
+
|
|
349
|
+
ctx.effect(() => {
|
|
350
|
+
return () => {
|
|
351
|
+
scheduler.stopAll();
|
|
352
|
+
};
|
|
353
|
+
}, 'dsh-cron: lifecycle');
|
|
354
|
+
|
|
355
|
+
const recommendations = [
|
|
356
|
+
{
|
|
357
|
+
id: 'rec_daily_digest',
|
|
358
|
+
title: 'Daily digest',
|
|
359
|
+
schedule: '0 8 * * 1-5',
|
|
360
|
+
scheduleText: 'Weekdays at 08:00',
|
|
361
|
+
prompt: 'Prepare a brief morning digest: review the list of active tasks, open Gitea tickets and outline the key priorities for today.',
|
|
362
|
+
description: 'Start every weekday with a summary of calendar, unread mail and priorities',
|
|
363
|
+
icon: 'bell'
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
id: 'rec_weekly_review',
|
|
367
|
+
title: 'Weekly review',
|
|
368
|
+
schedule: '0 16 * * 5',
|
|
369
|
+
scheduleText: 'Fridays at 16:00',
|
|
370
|
+
prompt: 'Prepare a weekly report on the work done: the list of closed tasks, created PRs and the current status of ongoing projects.',
|
|
371
|
+
description: 'Every Friday create a short report on the completed work',
|
|
372
|
+
icon: 'clipboard'
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
id: 'rec_monitor_status',
|
|
376
|
+
title: 'Follow-up monitor',
|
|
377
|
+
schedule: '0 9 * * 1-5',
|
|
378
|
+
scheduleText: 'Weekdays at 09:00',
|
|
379
|
+
prompt: 'Review recent project activity, build statuses and flag everything that requires developer attention.',
|
|
380
|
+
description: 'Review recent project activity and flag anything that needs your attention',
|
|
381
|
+
icon: 'activity'
|
|
163
382
|
}
|
|
164
|
-
|
|
383
|
+
];
|
|
384
|
+
|
|
385
|
+
// 1. REST API — collection (list/create) + item actions in one prefix
|
|
386
|
+
// handler per route family; registered WITHOUT the trailing slash (#96)
|
|
387
|
+
ctx.effect(() => ctx.webServer.register({
|
|
388
|
+
kind: 'prefix',
|
|
389
|
+
path: '/dsh-cron/tasks',
|
|
390
|
+
handler: createCronApiHandler(store, scheduler, { recommendations })
|
|
391
|
+
}), 'dsh-cron: /tasks[...]');
|
|
392
|
+
|
|
393
|
+
// Alias route /dsh-cron/action/:id/:action for backward compatibility (#66)
|
|
394
|
+
ctx.effect(() => ctx.webServer.register({
|
|
395
|
+
kind: 'prefix',
|
|
396
|
+
path: '/dsh-cron/action',
|
|
397
|
+
handler: createCronApiHandler(store, scheduler, null)
|
|
398
|
+
}), 'dsh-cron: /action/:id/:action');
|
|
165
399
|
|
|
166
400
|
// GET /dsh-cron/models
|
|
167
401
|
ctx.effect(() => ctx.webServer.register({
|
|
@@ -177,6 +411,7 @@ export function apply(ctx, config) {
|
|
|
177
411
|
kind: 'exact',
|
|
178
412
|
path: '/dsh-cron/chat/start',
|
|
179
413
|
handler: async (req, res) => {
|
|
414
|
+
if (req.method === 'POST' && rejectCrossOrigin(req, res)) return;
|
|
180
415
|
await chatStartHandler(ctx, req, res, parseJsonBody, sendJson, randomUUID);
|
|
181
416
|
}
|
|
182
417
|
}), 'dsh-cron: /chat/start');
|
|
@@ -192,14 +427,25 @@ export function apply(ctx, config) {
|
|
|
192
427
|
return;
|
|
193
428
|
}
|
|
194
429
|
if (req.method === 'POST') {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
430
|
+
if (rejectCrossOrigin(req, res)) return;
|
|
431
|
+
const { body, error } = await readBody(req, res);
|
|
432
|
+
if (error) return;
|
|
433
|
+
// Masked secrets echoed back by the client must never overwrite the
|
|
434
|
+
// stored token (store.saveSettings guards this too, #86-era).
|
|
435
|
+
const payload = Object.assign({}, body);
|
|
436
|
+
if (typeof payload.botToken === 'string' && (payload.botToken.includes('••') || payload.botToken.includes('****'))) {
|
|
437
|
+
delete payload.botToken;
|
|
438
|
+
}
|
|
439
|
+
const scopeErrors = await applySettingsToScope(cronSettingsScope, payload);
|
|
440
|
+
if (scopeErrors === null) {
|
|
441
|
+
store.saveSettings(payload);
|
|
442
|
+
}
|
|
443
|
+
sendJson(res, 200, { ok: true, settings: store.getClientSettings(), scopeErrors: scopeErrors || undefined });
|
|
198
444
|
return;
|
|
199
445
|
}
|
|
200
446
|
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
201
447
|
} catch (err) {
|
|
202
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
448
|
+
sendJson(res, err.statusCode || 500, { ok: false, error: err.message });
|
|
203
449
|
}
|
|
204
450
|
}
|
|
205
451
|
}), 'dsh-cron: /settings');
|
|
@@ -214,19 +460,21 @@ export function apply(ctx, config) {
|
|
|
214
460
|
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
215
461
|
return;
|
|
216
462
|
}
|
|
217
|
-
|
|
463
|
+
if (rejectCrossOrigin(req, res)) return;
|
|
464
|
+
const { body, error } = await readBody(req, res);
|
|
465
|
+
if (error) return;
|
|
218
466
|
const settings = store.getSettings();
|
|
219
467
|
const botToken = body.botToken || settings.botToken;
|
|
220
468
|
const chatId = body.chatId || settings.chatId;
|
|
221
469
|
|
|
222
470
|
if (!botToken || !chatId) {
|
|
223
|
-
sendJson(res, 400, { ok: false, error: '
|
|
471
|
+
sendJson(res, 400, { ok: false, error: 'botToken or chatId is not configured' });
|
|
224
472
|
return;
|
|
225
473
|
}
|
|
226
474
|
|
|
227
|
-
const text = `🔔
|
|
475
|
+
const text = `🔔 *dsh-cron test notification*\n\nConnection between DSH Cron and Telegram is working! Checked at: \`${new Date().toISOString()}\``;
|
|
228
476
|
await sendTelegramMessage({ botToken, chatId, text });
|
|
229
|
-
sendJson(res, 200, { ok: true, message: '
|
|
477
|
+
sendJson(res, 200, { ok: true, message: 'Test message sent successfully' });
|
|
230
478
|
} catch (err) {
|
|
231
479
|
sendJson(res, 500, { ok: false, error: err.message });
|
|
232
480
|
}
|
|
@@ -243,13 +491,15 @@ export function apply(ctx, config) {
|
|
|
243
491
|
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
244
492
|
return;
|
|
245
493
|
}
|
|
246
|
-
|
|
494
|
+
if (rejectCrossOrigin(req, res)) return;
|
|
495
|
+
const { body, error } = await readBody(req, res);
|
|
496
|
+
if (error) return;
|
|
247
497
|
const settings = store.getSettings();
|
|
248
498
|
const kanbanBaseUrl = body.kanbanBaseUrl || settings.kanbanBaseUrl || 'http://127.0.0.1:3000';
|
|
249
499
|
|
|
250
500
|
const result = await createKanbanCard({
|
|
251
|
-
title:
|
|
252
|
-
body:
|
|
501
|
+
title: '[Test] dsh-cron -> Kanban connectivity check',
|
|
502
|
+
body: `Test card created automatically by the \`dsh-cron\` module at ${new Date().toISOString()}.`,
|
|
253
503
|
board: 'main',
|
|
254
504
|
column: 'backlog',
|
|
255
505
|
labels: ['cron', 'test'],
|
|
@@ -267,289 +517,29 @@ export function apply(ctx, config) {
|
|
|
267
517
|
}
|
|
268
518
|
}), 'dsh-cron: /kanban/test');
|
|
269
519
|
|
|
270
|
-
// PATCH & DELETE /dsh-cron/tasks/:id
|
|
271
|
-
ctx.effect(() => ctx.webServer.register({
|
|
272
|
-
kind: 'prefix',
|
|
273
|
-
path: '/dsh-cron/tasks/',
|
|
274
|
-
handler: async (req, res) => {
|
|
275
|
-
try {
|
|
276
|
-
const url = new URL(req.url, 'http://127.0.0.1');
|
|
277
|
-
const parts = url.pathname.split('/').filter(Boolean);
|
|
278
|
-
const id = parts[2];
|
|
279
|
-
const action = parts[3];
|
|
280
|
-
|
|
281
|
-
if (!id) {
|
|
282
|
-
sendJson(res, 400, { ok: false, error: 'ID задачи не указан' });
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (req.method === 'GET' && action === 'history') {
|
|
287
|
-
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
288
|
-
const history = store.getHistory(id, limit);
|
|
289
|
-
sendJson(res, 200, { ok: true, history });
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (req.method === 'POST' && action === 'run') {
|
|
294
|
-
await scheduler.triggerManualRun(id);
|
|
295
|
-
const task = store.get(id);
|
|
296
|
-
sendJson(res, 200, { ok: true, task });
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (req.method === 'POST' && action === 'pause') {
|
|
301
|
-
const task = scheduler.pauseTask(id);
|
|
302
|
-
if (!task) {
|
|
303
|
-
sendJson(res, 404, { ok: false, error: 'Задача не найдена' });
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
sendJson(res, 200, { ok: true, task });
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (req.method === 'POST' && action === 'resume') {
|
|
311
|
-
const task = scheduler.resumeTask(id);
|
|
312
|
-
if (!task) {
|
|
313
|
-
sendJson(res, 404, { ok: false, error: 'Задача не найдена' });
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
sendJson(res, 200, { ok: true, task });
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (req.method === 'POST' && (action === 'toggle' || (!action && url.searchParams.get('action') === 'toggle'))) {
|
|
321
|
-
const current = store.get(id);
|
|
322
|
-
if (!current) {
|
|
323
|
-
sendJson(res, 404, { ok: false, error: 'Задача не найдена' });
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
const task = scheduler.toggleTask(id);
|
|
327
|
-
sendJson(res, 200, { ok: true, task });
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (req.method === 'PATCH') {
|
|
332
|
-
const body = await parseJsonBody(req);
|
|
333
|
-
const current = store.get(id);
|
|
334
|
-
if (!current) {
|
|
335
|
-
sendJson(res, 404, { ok: false, error: 'Задача не найдена' });
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
let patchData = { ...body };
|
|
339
|
-
if (body.schedule) {
|
|
340
|
-
const parsed = parseScheduleExpression(body.schedule);
|
|
341
|
-
patchData.schedule = parsed.cronPattern || body.schedule;
|
|
342
|
-
patchData.scheduleText = body.scheduleText || parsed.humanText;
|
|
343
|
-
patchData.oneShot = Boolean(parsed.isOneShot || body.oneShot);
|
|
344
|
-
}
|
|
345
|
-
const task = store.set({ ...current, ...patchData, id });
|
|
346
|
-
if (task.status === 'active') {
|
|
347
|
-
scheduler.scheduleTask(task);
|
|
348
|
-
} else {
|
|
349
|
-
scheduler.pauseTask(id);
|
|
350
|
-
}
|
|
351
|
-
sendJson(res, 200, { ok: true, task });
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (req.method === 'DELETE' || (req.method === 'POST' && action === 'delete')) {
|
|
356
|
-
const current = store.get(id);
|
|
357
|
-
if (current) {
|
|
358
|
-
scheduler.pauseTask(id);
|
|
359
|
-
store.delete(id);
|
|
360
|
-
}
|
|
361
|
-
sendJson(res, 200, { ok: true });
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
366
|
-
} catch (err) {
|
|
367
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}), 'dsh-cron: /tasks/:id');
|
|
371
|
-
|
|
372
|
-
// Alias route /dsh-cron/action/:id/:action for backward compatibility (#66)
|
|
373
|
-
ctx.effect(() => ctx.webServer.register({
|
|
374
|
-
kind: 'prefix',
|
|
375
|
-
path: '/dsh-cron/action/',
|
|
376
|
-
handler: async (req, res) => {
|
|
377
|
-
try {
|
|
378
|
-
const url = new URL(req.url, 'http://127.0.0.1');
|
|
379
|
-
const parts = url.pathname.split('/').filter(Boolean);
|
|
380
|
-
const id = parts[2];
|
|
381
|
-
const action = parts[3];
|
|
382
|
-
|
|
383
|
-
if (!id) {
|
|
384
|
-
sendJson(res, 400, { ok: false, error: 'ID задачи не указан' });
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (action === 'history' || (req.method === 'GET' && !action)) {
|
|
389
|
-
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
390
|
-
const history = store.getHistory(id, limit);
|
|
391
|
-
sendJson(res, 200, { ok: true, history });
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (action === 'run') {
|
|
396
|
-
await scheduler.triggerManualRun(id);
|
|
397
|
-
const task = store.get(id);
|
|
398
|
-
sendJson(res, 200, { ok: true, task });
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
if (action === 'toggle') {
|
|
403
|
-
const current = store.get(id);
|
|
404
|
-
if (!current) {
|
|
405
|
-
sendJson(res, 404, { ok: false, error: 'Задача не найдена' });
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
const task = scheduler.toggleTask(id);
|
|
409
|
-
sendJson(res, 200, { ok: true, task });
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (action === 'delete') {
|
|
414
|
-
const current = store.get(id);
|
|
415
|
-
if (current) {
|
|
416
|
-
scheduler.pauseTask(id);
|
|
417
|
-
store.delete(id);
|
|
418
|
-
}
|
|
419
|
-
sendJson(res, 200, { ok: true });
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
sendJson(res, 405, { ok: false, error: 'Action not supported' });
|
|
424
|
-
} catch (err) {
|
|
425
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}), 'dsh-cron: /action/:id/:action');
|
|
429
|
-
|
|
430
520
|
// 2. Tools
|
|
431
521
|
ctx.tools.register(defineTool({
|
|
432
522
|
name: 'cron_create_task',
|
|
433
|
-
description: '
|
|
434
|
-
parameters:
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
prompt: { type: 'string', description: 'Промпт/инструкция для агента при запуске', required: true },
|
|
438
|
-
type: { type: 'string', enum: ['prompt', 'script'], description: 'Тип задачи' },
|
|
439
|
-
delivery: { type: 'string', enum: ['current', 'isolated'], description: 'Режим запуска: current (в текущем чате) или isolated (изолированная сессия)' },
|
|
440
|
-
provider: { type: 'string', description: 'Провайдер модели (опционально)' },
|
|
441
|
-
model: { type: 'string', description: 'Модель (опционально)' },
|
|
442
|
-
notifyTelegram: { type: 'boolean', description: 'Отправлять отчет в Telegram' },
|
|
443
|
-
onlyOnFailure: { type: 'boolean', description: 'Отправлять отчет только при сбоях' },
|
|
444
|
-
timeoutSeconds: { type: 'number', description: 'Лимит времени выполнения в секундах' },
|
|
445
|
-
overlapPolicy: { type: 'string', enum: ['skip', 'queue', 'replace'], description: 'Политика перекрытия' },
|
|
446
|
-
kanbanMode: { type: 'string', enum: ['none', 'on_failure', 'always'], description: 'Создавать карточку в Kanban' },
|
|
447
|
-
},
|
|
448
|
-
output: {
|
|
449
|
-
schema: {
|
|
450
|
-
type: 'object',
|
|
451
|
-
additionalProperties: true,
|
|
452
|
-
properties: {
|
|
453
|
-
success: { type: 'boolean' },
|
|
454
|
-
message: { type: 'string' },
|
|
455
|
-
task: { type: 'object', additionalProperties: true }
|
|
456
|
-
}
|
|
457
|
-
},
|
|
458
|
-
render: (_args, val) => [{ type: 'text', text: val.message || JSON.stringify(val) }]
|
|
459
|
-
},
|
|
460
|
-
execute: async (args) => {
|
|
461
|
-
const taskType = args.type === 'script' ? 'script' : 'llm';
|
|
462
|
-
const parsed = parseScheduleExpression(args.schedule);
|
|
463
|
-
const task = store.set({
|
|
464
|
-
title: args.title,
|
|
465
|
-
schedule: parsed.cronPattern || args.schedule,
|
|
466
|
-
scheduleText: parsed.humanText,
|
|
467
|
-
prompt: args.prompt,
|
|
468
|
-
type: taskType,
|
|
469
|
-
delivery: args.delivery || 'isolated',
|
|
470
|
-
status: 'active',
|
|
471
|
-
provider: args.provider,
|
|
472
|
-
model: args.model,
|
|
473
|
-
notifyTelegram: Boolean(args.notifyTelegram),
|
|
474
|
-
onlyOnFailure: Boolean(args.onlyOnFailure),
|
|
475
|
-
timeoutSeconds: Number(args.timeoutSeconds) || 1800,
|
|
476
|
-
overlapPolicy: args.overlapPolicy || 'skip',
|
|
477
|
-
kanbanMode: args.kanbanMode || 'none',
|
|
478
|
-
oneShot: Boolean(parsed.isOneShot),
|
|
479
|
-
});
|
|
480
|
-
scheduler.scheduleTask(task);
|
|
481
|
-
return {
|
|
482
|
-
success: true,
|
|
483
|
-
message: `Задача "${task.title}" успешно запланирована (${task.scheduleText})`,
|
|
484
|
-
task
|
|
485
|
-
};
|
|
486
|
-
}
|
|
523
|
+
description: 'Create a new scheduled cron task (cron schedule, or one-shot via "at: <ISO timestamp>" or "in 20m")',
|
|
524
|
+
parameters: createTaskParameters,
|
|
525
|
+
output: createTaskOutput,
|
|
526
|
+
execute: async (args) => executeCreateTask(store, scheduler, args)
|
|
487
527
|
}));
|
|
488
528
|
|
|
489
529
|
// Alias tool cron_schedule_task for compatibility with agent prompts (#70)
|
|
490
530
|
ctx.tools.register(defineTool({
|
|
491
531
|
name: 'cron_schedule_task',
|
|
492
|
-
description: '
|
|
493
|
-
parameters:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
prompt: { type: 'string', description: 'Промпт/инструкция для агента при запуске', required: true },
|
|
497
|
-
type: { type: 'string', enum: ['llm', 'prompt', 'script'], description: 'Тип задачи' },
|
|
498
|
-
delivery: { type: 'string', enum: ['current', 'isolated'], description: 'Режим запуска' },
|
|
499
|
-
provider: { type: 'string', description: 'Провайдер модели (опционально)' },
|
|
500
|
-
model: { type: 'string', description: 'Модель (опционально)' },
|
|
501
|
-
notifyTelegram: { type: 'boolean', description: 'Отправлять отчет в Telegram' },
|
|
502
|
-
onlyOnFailure: { type: 'boolean', description: 'Отправлять отчет только при сбоях' },
|
|
503
|
-
timeoutSeconds: { type: 'number', description: 'Лимит времени выполнения в секундах' },
|
|
504
|
-
overlapPolicy: { type: 'string', enum: ['skip', 'queue', 'replace'], description: 'Политика перекрытия' },
|
|
505
|
-
kanbanMode: { type: 'string', enum: ['none', 'on_failure', 'always'], description: 'Создавать карточку в Kanban' },
|
|
506
|
-
},
|
|
507
|
-
output: {
|
|
508
|
-
schema: {
|
|
509
|
-
type: 'object',
|
|
510
|
-
additionalProperties: true,
|
|
511
|
-
properties: {
|
|
512
|
-
success: { type: 'boolean' },
|
|
513
|
-
message: { type: 'string' },
|
|
514
|
-
task: { type: 'object', additionalProperties: true }
|
|
515
|
-
}
|
|
516
|
-
},
|
|
517
|
-
render: (_args, val) => [{ type: 'text', text: val.message || JSON.stringify(val) }]
|
|
518
|
-
},
|
|
519
|
-
execute: async (args) => {
|
|
520
|
-
const taskType = args.type === 'script' ? 'script' : 'llm';
|
|
521
|
-
const parsed = parseScheduleExpression(args.schedule);
|
|
522
|
-
const task = store.set({
|
|
523
|
-
title: args.title,
|
|
524
|
-
schedule: parsed.cronPattern || args.schedule,
|
|
525
|
-
scheduleText: parsed.humanText,
|
|
526
|
-
prompt: args.prompt,
|
|
527
|
-
type: taskType,
|
|
528
|
-
delivery: args.delivery || 'isolated',
|
|
529
|
-
status: 'active',
|
|
530
|
-
provider: args.provider,
|
|
531
|
-
model: args.model,
|
|
532
|
-
notifyTelegram: Boolean(args.notifyTelegram),
|
|
533
|
-
onlyOnFailure: Boolean(args.onlyOnFailure),
|
|
534
|
-
timeoutSeconds: Number(args.timeoutSeconds) || 1800,
|
|
535
|
-
overlapPolicy: args.overlapPolicy || 'skip',
|
|
536
|
-
kanbanMode: args.kanbanMode || 'none',
|
|
537
|
-
oneShot: Boolean(parsed.isOneShot),
|
|
538
|
-
});
|
|
539
|
-
scheduler.scheduleTask(task);
|
|
540
|
-
return {
|
|
541
|
-
success: true,
|
|
542
|
-
message: `Задача "${task.title}" успешно запланирована (${task.scheduleText})`,
|
|
543
|
-
task
|
|
544
|
-
};
|
|
545
|
-
}
|
|
532
|
+
description: 'Alias for cron_create_task: create a new scheduled cron task',
|
|
533
|
+
parameters: createTaskParameters,
|
|
534
|
+
output: createTaskOutput,
|
|
535
|
+
execute: async (args) => executeCreateTask(store, scheduler, args)
|
|
546
536
|
}));
|
|
547
537
|
|
|
548
538
|
ctx.tools.register(defineTool({
|
|
549
539
|
name: 'cron_list_tasks',
|
|
550
|
-
description: '
|
|
540
|
+
description: 'List all scheduled cron tasks',
|
|
551
541
|
parameters: {
|
|
552
|
-
status: { type: 'string', enum: ['all', 'active', 'paused', 'completed'], description: '
|
|
542
|
+
status: { type: 'string', enum: ['all', 'active', 'paused', 'completed'], description: 'Filter by status' }
|
|
553
543
|
},
|
|
554
544
|
output: {
|
|
555
545
|
schema: {
|
|
@@ -560,7 +550,7 @@ export function apply(ctx, config) {
|
|
|
560
550
|
tasks: { type: 'array', items: { type: 'object', additionalProperties: true } }
|
|
561
551
|
}
|
|
562
552
|
},
|
|
563
|
-
render: (_args, val) => [{ type: 'text', text:
|
|
553
|
+
render: (_args, val) => [{ type: 'text', text: `Total tasks: ${val.count}` }]
|
|
564
554
|
},
|
|
565
555
|
execute: async (args) => {
|
|
566
556
|
const tasks = store.list({ status: args.status || 'all' });
|
|
@@ -583,9 +573,9 @@ export function apply(ctx, config) {
|
|
|
583
573
|
|
|
584
574
|
ctx.tools.register(defineTool({
|
|
585
575
|
name: 'cron_pause_task',
|
|
586
|
-
description: '
|
|
576
|
+
description: 'Pause a task by its ID',
|
|
587
577
|
parameters: {
|
|
588
|
-
id: { type: 'string', description: '
|
|
578
|
+
id: { type: 'string', description: 'Task identifier', required: true }
|
|
589
579
|
},
|
|
590
580
|
output: {
|
|
591
581
|
schema: {
|
|
@@ -600,16 +590,16 @@ export function apply(ctx, config) {
|
|
|
600
590
|
},
|
|
601
591
|
execute: async (args) => {
|
|
602
592
|
const task = scheduler.pauseTask(args.id);
|
|
603
|
-
if (!task) return { success: false, message: '
|
|
604
|
-
return { success: true, message:
|
|
593
|
+
if (!task) return { success: false, message: 'Task not found' };
|
|
594
|
+
return { success: true, message: `Task "${task.title}" paused` };
|
|
605
595
|
}
|
|
606
596
|
}));
|
|
607
597
|
|
|
608
598
|
ctx.tools.register(defineTool({
|
|
609
599
|
name: 'cron_resume_task',
|
|
610
|
-
description: '
|
|
600
|
+
description: 'Resume a paused task',
|
|
611
601
|
parameters: {
|
|
612
|
-
id: { type: 'string', description: '
|
|
602
|
+
id: { type: 'string', description: 'Task identifier', required: true }
|
|
613
603
|
},
|
|
614
604
|
output: {
|
|
615
605
|
schema: {
|
|
@@ -624,16 +614,16 @@ export function apply(ctx, config) {
|
|
|
624
614
|
},
|
|
625
615
|
execute: async (args) => {
|
|
626
616
|
const task = scheduler.resumeTask(args.id);
|
|
627
|
-
if (!task) return { success: false, message: '
|
|
628
|
-
return { success: true, message:
|
|
617
|
+
if (!task) return { success: false, message: 'Task not found' };
|
|
618
|
+
return { success: true, message: `Task "${task.title}" resumed (${task.scheduleText})` };
|
|
629
619
|
}
|
|
630
620
|
}));
|
|
631
621
|
|
|
632
622
|
ctx.tools.register(defineTool({
|
|
633
623
|
name: 'cron_delete_task',
|
|
634
|
-
description: '
|
|
624
|
+
description: 'Delete a task by its ID',
|
|
635
625
|
parameters: {
|
|
636
|
-
id: { type: 'string', description: '
|
|
626
|
+
id: { type: 'string', description: 'Task identifier', required: true }
|
|
637
627
|
},
|
|
638
628
|
output: {
|
|
639
629
|
schema: {
|
|
@@ -649,15 +639,15 @@ export function apply(ctx, config) {
|
|
|
649
639
|
execute: async (args) => {
|
|
650
640
|
scheduler.pauseTask(args.id);
|
|
651
641
|
const ok = store.delete(args.id);
|
|
652
|
-
return { success: ok, message: ok ? '
|
|
642
|
+
return { success: ok, message: ok ? 'Task deleted' : 'Task not found' };
|
|
653
643
|
}
|
|
654
644
|
}));
|
|
655
645
|
|
|
656
646
|
ctx.tools.register(defineTool({
|
|
657
647
|
name: 'cron_run_task',
|
|
658
|
-
description: '
|
|
648
|
+
description: 'Trigger a task immediately, out of band',
|
|
659
649
|
parameters: {
|
|
660
|
-
id: { type: 'string', description: '
|
|
650
|
+
id: { type: 'string', description: 'Task identifier', required: true }
|
|
661
651
|
},
|
|
662
652
|
output: {
|
|
663
653
|
schema: {
|
|
@@ -672,9 +662,9 @@ export function apply(ctx, config) {
|
|
|
672
662
|
},
|
|
673
663
|
execute: async (args) => {
|
|
674
664
|
const task = store.get(args.id);
|
|
675
|
-
if (!task) return { success: false, message: '
|
|
665
|
+
if (!task) return { success: false, message: 'Task not found' };
|
|
676
666
|
scheduler.triggerManualRun(args.id);
|
|
677
|
-
return { success: true, message:
|
|
667
|
+
return { success: true, message: `Manual run started for task "${task.title}"` };
|
|
678
668
|
}
|
|
679
669
|
}));
|
|
680
670
|
}
|