@goodandready/dsh-cron 0.2.28 → 0.2.30
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 +42 -20
- package/README.ru.md +41 -18
- package/README.zh.md +41 -18
- package/lib/api-import-export.js +4 -3
- package/lib/api-webhook.js +3 -2
- package/lib/api.js +10 -2
- package/lib/burn-guard.js +2 -1
- package/lib/client.js +116 -113
- package/lib/cron-tool.js +402 -0
- package/lib/index.js +32 -298
- package/lib/integrations.js +2 -1
- package/lib/llm-actions.js +2 -1
- package/lib/logger.js +38 -0
- package/lib/models-handler.js +4 -0
- package/lib/prompt.js +7 -4
- package/lib/routes.js +5 -1
- package/lib/runner-external.js +3 -2
- package/lib/runner-worktree.js +2 -1
- package/lib/runner.js +12 -11
- package/lib/scheduler-execution.js +27 -26
- package/lib/scheduler-lifecycle.js +3 -2
- package/lib/scheduler.js +5 -4
- package/lib/settings.js +3 -2
- package/lib/store.js +11 -10
- package/lib/task-patch.js +1 -1
- package/lib/telegram.js +4 -3
- package/package.json +2 -2
package/lib/cron-tool.js
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { executeCreateTask } from './task-create.js';
|
|
2
|
+
import { applyTaskPatch, describeTaskPatch } from './task-patch.js';
|
|
3
|
+
import { isConfigOwned, configOwnedMessage } from './config-jobs.js';
|
|
4
|
+
import { TASK_TYPES } from './runtimes.js';
|
|
5
|
+
import { CHANNEL_IDS } from './channels.js';
|
|
6
|
+
|
|
7
|
+
export const LEGACY_TOOL_NAMES = [
|
|
8
|
+
'cron_create_task',
|
|
9
|
+
'cron_schedule_task',
|
|
10
|
+
'cron_list_tasks',
|
|
11
|
+
'cron_pause_task',
|
|
12
|
+
'cron_resume_task',
|
|
13
|
+
'cron_delete_task',
|
|
14
|
+
'cron_get_task',
|
|
15
|
+
'cron_update_task',
|
|
16
|
+
'cron_run_task',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export const LEGACY_ACTION_MAP = {
|
|
20
|
+
cron_create_task: 'create',
|
|
21
|
+
create_task: 'create',
|
|
22
|
+
cron_schedule_task: 'create',
|
|
23
|
+
schedule_task: 'create',
|
|
24
|
+
schedule: 'create',
|
|
25
|
+
cron_list_tasks: 'list',
|
|
26
|
+
list_tasks: 'list',
|
|
27
|
+
cron_get_task: 'get',
|
|
28
|
+
get_task: 'get',
|
|
29
|
+
cron_update_task: 'update',
|
|
30
|
+
update_task: 'update',
|
|
31
|
+
cron_pause_task: 'pause',
|
|
32
|
+
pause_task: 'pause',
|
|
33
|
+
cron_resume_task: 'resume',
|
|
34
|
+
resume_task: 'resume',
|
|
35
|
+
cron_run_task: 'run',
|
|
36
|
+
run_task: 'run',
|
|
37
|
+
cron_delete_task: 'delete',
|
|
38
|
+
delete_task: 'delete',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function rejectLegacyTool(toolName) {
|
|
42
|
+
const mapped = LEGACY_ACTION_MAP[toolName] || 'list';
|
|
43
|
+
return {
|
|
44
|
+
success: false,
|
|
45
|
+
message: `Tool "${toolName}" has been removed and consolidated into the unified "cron" tool. Please call "cron" with action: "${mapped}" instead. For simple in-chat reminders, use the built-in "schedule_create" tool.`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const cronToolParameters = {
|
|
50
|
+
action: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
enum: ['create', 'list', 'get', 'update', 'pause', 'resume', 'run', 'delete'],
|
|
53
|
+
description: 'Operation to perform: "create", "list", "get", "update", "pause", "resume", "run", or "delete". For simple reminders in the current chat, use the built-in "schedule_create" tool instead.',
|
|
54
|
+
required: true,
|
|
55
|
+
},
|
|
56
|
+
id: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description: 'Task ID (required for get, update, pause, resume, run, delete)',
|
|
59
|
+
},
|
|
60
|
+
title: {
|
|
61
|
+
type: 'string',
|
|
62
|
+
description: 'Short task name (for create and update)',
|
|
63
|
+
},
|
|
64
|
+
schedule: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
description: 'Schedule expression: cron ("0 9 * * *"), shorthand ("@daily", "@every 30m"), interval ("every 2h"), or one-shot ("in 30m", "at: 2026-09-05T15:00:00Z")',
|
|
67
|
+
},
|
|
68
|
+
prompt: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: 'Prompt/instruction for the agent at run time or command to execute',
|
|
71
|
+
},
|
|
72
|
+
type: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
enum: TASK_TYPES,
|
|
75
|
+
description: 'Task runtime type: llm | script | node | python | http | ssh | docker | skill | workflow',
|
|
76
|
+
},
|
|
77
|
+
status: {
|
|
78
|
+
type: 'string',
|
|
79
|
+
enum: ['all', 'active', 'paused', 'completed'],
|
|
80
|
+
description: 'Filter tasks by status (for list action)',
|
|
81
|
+
},
|
|
82
|
+
confirmCodeSwitch: {
|
|
83
|
+
type: 'boolean',
|
|
84
|
+
description: 'Explicit confirmation required when updating a task into a code-executing type (script/node/python/ssh/docker)',
|
|
85
|
+
},
|
|
86
|
+
delivery: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
enum: ['current', 'isolated'],
|
|
89
|
+
description: 'Run mode: current (in current chat) or isolated (separate session)',
|
|
90
|
+
},
|
|
91
|
+
provider: {
|
|
92
|
+
type: 'string',
|
|
93
|
+
description: 'Model provider (optional)',
|
|
94
|
+
},
|
|
95
|
+
model: {
|
|
96
|
+
type: 'string',
|
|
97
|
+
description: 'Model identifier (optional)',
|
|
98
|
+
},
|
|
99
|
+
fallbackModel: {
|
|
100
|
+
type: 'string',
|
|
101
|
+
description: 'Model tried once more when the primary model fails',
|
|
102
|
+
},
|
|
103
|
+
fallbackProvider: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
description: 'Provider for the fallback model (defaults to task provider)',
|
|
106
|
+
},
|
|
107
|
+
inspectOnFailure: {
|
|
108
|
+
type: 'boolean',
|
|
109
|
+
description: 'Diagnose failed runs with a model and store the diagnosis with the run',
|
|
110
|
+
},
|
|
111
|
+
silentRule: {
|
|
112
|
+
type: 'string',
|
|
113
|
+
description: 'Plain-language condition for staying silent on a successful run',
|
|
114
|
+
},
|
|
115
|
+
timezone: {
|
|
116
|
+
type: 'string',
|
|
117
|
+
description: 'IANA time zone for the schedule, e.g. "Europe/Berlin"',
|
|
118
|
+
},
|
|
119
|
+
notifyTelegram: {
|
|
120
|
+
type: 'boolean',
|
|
121
|
+
description: 'Send the run report to Telegram',
|
|
122
|
+
},
|
|
123
|
+
onlyOnFailure: {
|
|
124
|
+
type: 'boolean',
|
|
125
|
+
description: 'Send reports only for failures',
|
|
126
|
+
},
|
|
127
|
+
timeoutSeconds: {
|
|
128
|
+
type: 'number',
|
|
129
|
+
description: 'Execution time limit in seconds',
|
|
130
|
+
},
|
|
131
|
+
overlapPolicy: {
|
|
132
|
+
type: 'string',
|
|
133
|
+
enum: ['skip', 'queue', 'replace'],
|
|
134
|
+
description: 'Overlap policy: skip | queue | replace',
|
|
135
|
+
},
|
|
136
|
+
misfirePolicy: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
enum: ['skip', 'runOnce', 'catchUpAll'],
|
|
139
|
+
description: 'What to do with a run missed during downtime: skip | runOnce | catchUpAll',
|
|
140
|
+
},
|
|
141
|
+
maxRetries: {
|
|
142
|
+
type: 'number',
|
|
143
|
+
description: 'Automatic retry attempts on failure (0 = off)',
|
|
144
|
+
},
|
|
145
|
+
retryBackoffMs: {
|
|
146
|
+
type: 'number',
|
|
147
|
+
description: 'Base backoff between retries in ms, doubled per attempt',
|
|
148
|
+
},
|
|
149
|
+
permissionPreset: {
|
|
150
|
+
type: 'string',
|
|
151
|
+
enum: ['default', 'read-only', 'workspace-write', 'full'],
|
|
152
|
+
description: 'Permission preset applied to the task session',
|
|
153
|
+
},
|
|
154
|
+
kanbanMode: {
|
|
155
|
+
type: 'string',
|
|
156
|
+
enum: ['none', 'on_failure', 'always'],
|
|
157
|
+
description: 'Create a dsh-kanban card for runs: none | on_failure | always',
|
|
158
|
+
},
|
|
159
|
+
channels: {
|
|
160
|
+
type: 'array',
|
|
161
|
+
items: { type: 'string', enum: CHANNEL_IDS },
|
|
162
|
+
description: 'Notification channels for this task',
|
|
163
|
+
},
|
|
164
|
+
template: {
|
|
165
|
+
type: 'string',
|
|
166
|
+
description: 'Notification template with {title} {status} {output} {error} {duration} placeholders',
|
|
167
|
+
},
|
|
168
|
+
env: {
|
|
169
|
+
type: 'object',
|
|
170
|
+
additionalProperties: true,
|
|
171
|
+
description: 'Environment variables for external runtimes (KEY: value)',
|
|
172
|
+
},
|
|
173
|
+
cwd: {
|
|
174
|
+
type: 'string',
|
|
175
|
+
description: 'Working directory for the run',
|
|
176
|
+
},
|
|
177
|
+
workspaceId: {
|
|
178
|
+
type: 'string',
|
|
179
|
+
description: 'Workspace bound to the task',
|
|
180
|
+
},
|
|
181
|
+
worktree: {
|
|
182
|
+
type: 'boolean',
|
|
183
|
+
description: 'Run code-modifying agent tasks in an isolated git worktree',
|
|
184
|
+
},
|
|
185
|
+
keepWorktree: {
|
|
186
|
+
type: 'boolean',
|
|
187
|
+
description: 'Keep the created worktree after the run',
|
|
188
|
+
},
|
|
189
|
+
nodePath: {
|
|
190
|
+
type: 'string',
|
|
191
|
+
description: 'Node.js binary for node tasks',
|
|
192
|
+
},
|
|
193
|
+
pythonPath: {
|
|
194
|
+
type: 'string',
|
|
195
|
+
description: 'Python interpreter for python tasks',
|
|
196
|
+
},
|
|
197
|
+
httpMethod: {
|
|
198
|
+
type: 'string',
|
|
199
|
+
description: 'HTTP method for http tasks',
|
|
200
|
+
},
|
|
201
|
+
httpUrl: {
|
|
202
|
+
type: 'string',
|
|
203
|
+
description: 'URL for http tasks',
|
|
204
|
+
},
|
|
205
|
+
httpHeaders: {
|
|
206
|
+
type: 'object',
|
|
207
|
+
additionalProperties: true,
|
|
208
|
+
description: 'Headers for http tasks',
|
|
209
|
+
},
|
|
210
|
+
httpBody: {
|
|
211
|
+
type: 'string',
|
|
212
|
+
description: 'Request body for http tasks',
|
|
213
|
+
},
|
|
214
|
+
sshProfileId: {
|
|
215
|
+
type: 'string',
|
|
216
|
+
description: 'dsh-remote-workspace profile id',
|
|
217
|
+
},
|
|
218
|
+
sshTarget: {
|
|
219
|
+
type: 'string',
|
|
220
|
+
description: 'Fallback user@host when no remote-workspace profile is used',
|
|
221
|
+
},
|
|
222
|
+
sshPort: {
|
|
223
|
+
type: 'number',
|
|
224
|
+
description: 'SSH port',
|
|
225
|
+
},
|
|
226
|
+
sshKeyPath: {
|
|
227
|
+
type: 'string',
|
|
228
|
+
description: 'Private key path',
|
|
229
|
+
},
|
|
230
|
+
dockerImage: {
|
|
231
|
+
type: 'string',
|
|
232
|
+
description: 'Container image for docker tasks',
|
|
233
|
+
},
|
|
234
|
+
skillName: {
|
|
235
|
+
type: 'string',
|
|
236
|
+
description: 'DSH skill name for skill tasks',
|
|
237
|
+
},
|
|
238
|
+
workflowName: {
|
|
239
|
+
type: 'string',
|
|
240
|
+
description: 'DSH workflow name for workflow tasks',
|
|
241
|
+
},
|
|
242
|
+
costLimitUsd: {
|
|
243
|
+
type: 'number',
|
|
244
|
+
description: 'Maximum allowed total cost in USD before auto-pausing',
|
|
245
|
+
},
|
|
246
|
+
dailyCostLimitUsd: {
|
|
247
|
+
type: 'number',
|
|
248
|
+
description: 'Maximum allowed daily cost in USD before auto-pausing',
|
|
249
|
+
},
|
|
250
|
+
tokenLimit: {
|
|
251
|
+
type: 'number',
|
|
252
|
+
description: 'Maximum allowed token usage before auto-pausing',
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export const cronToolOutput = {
|
|
257
|
+
schema: {
|
|
258
|
+
type: 'object',
|
|
259
|
+
additionalProperties: true,
|
|
260
|
+
properties: {
|
|
261
|
+
success: { type: 'boolean' },
|
|
262
|
+
message: { type: 'string' },
|
|
263
|
+
count: { type: 'number' },
|
|
264
|
+
task: { type: 'object', additionalProperties: true },
|
|
265
|
+
tasks: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
render: (_args, val) => [{ type: 'text', text: val.message || (val.success ? 'Success' : 'Failed') }],
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export async function executeCronTool(store, scheduler, args = {}) {
|
|
272
|
+
const action = args.action;
|
|
273
|
+
|
|
274
|
+
if (!action) {
|
|
275
|
+
if (args.schedule && args.prompt) {
|
|
276
|
+
return {
|
|
277
|
+
success: false,
|
|
278
|
+
message: 'Missing "action" parameter. The 9 separate cron tools have been consolidated into "cron". Did you mean action: "create"?',
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
success: false,
|
|
283
|
+
message: 'Parameter "action" is required: "create", "list", "get", "update", "pause", "resume", "run", or "delete". Note: For simple in-chat reminders, use built-in "schedule_create".',
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (LEGACY_ACTION_MAP[action]) {
|
|
288
|
+
const suggested = LEGACY_ACTION_MAP[action];
|
|
289
|
+
return {
|
|
290
|
+
success: false,
|
|
291
|
+
message: `Action "${action}" is deprecated. The 9 separate tools have been consolidated into the unified "cron" tool. Please use action: "${suggested}".`,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
switch (action) {
|
|
296
|
+
case 'create': {
|
|
297
|
+
if (!args.title || !args.schedule || !args.prompt) {
|
|
298
|
+
return {
|
|
299
|
+
success: false,
|
|
300
|
+
message: 'Fields "title", "schedule", and "prompt" are required to create a task.',
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
return executeCreateTask(store, scheduler, args);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
return { success: false, message: err.message };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
case 'list': {
|
|
311
|
+
const tasks = store.list({ status: args.status || 'all' });
|
|
312
|
+
return {
|
|
313
|
+
success: true,
|
|
314
|
+
count: tasks.length,
|
|
315
|
+
tasks: tasks.map((t) => ({
|
|
316
|
+
id: t.id,
|
|
317
|
+
title: t.title,
|
|
318
|
+
schedule: t.scheduleText || t.schedule,
|
|
319
|
+
status: t.status,
|
|
320
|
+
nextRunAt: t.nextRunAt ? new Date(t.nextRunAt).toISOString() : null,
|
|
321
|
+
lastStatus: t.lastStatus,
|
|
322
|
+
totalTokens: t.totalTokens || 0,
|
|
323
|
+
totalCostUsd: t.totalCostUsd || 0,
|
|
324
|
+
oneShot: Boolean(t.oneShot),
|
|
325
|
+
})),
|
|
326
|
+
message: `Total tasks: ${tasks.length}`,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
case 'get': {
|
|
331
|
+
if (!args.id) return { success: false, message: 'Field "id" is required for get action' };
|
|
332
|
+
const task = store.get(args.id);
|
|
333
|
+
if (!task) return { success: false, message: 'Task not found' };
|
|
334
|
+
const summary = [
|
|
335
|
+
`Task "${task.title}" (${task.id})`,
|
|
336
|
+
`type: ${task.type || 'llm'}`,
|
|
337
|
+
`schedule: ${task.scheduleText || task.schedule}`,
|
|
338
|
+
`status: ${task.status}`,
|
|
339
|
+
task.type === 'llm' || !task.type ? `model: ${task.model || '(default)'}${task.fallbackModel ? ` (fallback: ${task.fallbackModel})` : ''}` : null,
|
|
340
|
+
`channels: ${Array.isArray(task.channels) && task.channels.length ? task.channels.join(', ') : '(legacy flags)'}`,
|
|
341
|
+
`prompt: ${String(task.prompt || '').slice(0, 500)}`,
|
|
342
|
+
].filter(Boolean).join('\n');
|
|
343
|
+
return { success: true, message: summary, task };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
case 'update': {
|
|
347
|
+
if (!args.id) return { success: false, message: 'Field "id" is required for update action' };
|
|
348
|
+
const { id, confirmCodeSwitch, action: _act, ...patch } = args;
|
|
349
|
+
const result = applyTaskPatch({
|
|
350
|
+
store,
|
|
351
|
+
scheduler,
|
|
352
|
+
id,
|
|
353
|
+
body: patch,
|
|
354
|
+
allowCodeSwitch: confirmCodeSwitch === true,
|
|
355
|
+
});
|
|
356
|
+
if (!result.ok) return { success: false, message: result.error };
|
|
357
|
+
return {
|
|
358
|
+
success: true,
|
|
359
|
+
message: `Task "${result.task.title}" updated (${result.task.scheduleText || result.task.schedule}). Changed: ${describeTaskPatch(result.task, result.patch)}`,
|
|
360
|
+
task: result.task,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
case 'pause': {
|
|
365
|
+
if (!args.id) return { success: false, message: 'Field "id" is required for pause action' };
|
|
366
|
+
if (isConfigOwned(store.get(args.id))) return { success: false, message: configOwnedMessage(args.id) };
|
|
367
|
+
const task = scheduler.pauseTask(args.id);
|
|
368
|
+
if (!task) return { success: false, message: 'Task not found' };
|
|
369
|
+
return { success: true, message: `Task "${task.title}" paused` };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
case 'resume': {
|
|
373
|
+
if (!args.id) return { success: false, message: 'Field "id" is required for resume action' };
|
|
374
|
+
if (isConfigOwned(store.get(args.id))) return { success: false, message: configOwnedMessage(args.id) };
|
|
375
|
+
const task = scheduler.resumeTask(args.id);
|
|
376
|
+
if (!task) return { success: false, message: 'Task not found' };
|
|
377
|
+
return { success: true, message: `Task "${task.title}" resumed (${task.scheduleText})` };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
case 'run': {
|
|
381
|
+
if (!args.id) return { success: false, message: 'Field "id" is required for run action' };
|
|
382
|
+
const task = store.get(args.id);
|
|
383
|
+
if (!task) return { success: false, message: 'Task not found' };
|
|
384
|
+
scheduler.triggerManualRun(args.id);
|
|
385
|
+
return { success: true, message: `Manual run started for task "${task.title}"` };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
case 'delete': {
|
|
389
|
+
if (!args.id) return { success: false, message: 'Field "id" is required for delete action' };
|
|
390
|
+
if (isConfigOwned(store.get(args.id))) return { success: false, message: configOwnedMessage(args.id) };
|
|
391
|
+
scheduler.pauseTask(args.id);
|
|
392
|
+
const ok = store.delete(args.id);
|
|
393
|
+
return { success: ok, message: ok ? 'Task deleted' : 'Task not found' };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
default:
|
|
397
|
+
return {
|
|
398
|
+
success: false,
|
|
399
|
+
message: `Unknown action "${action}". Allowed actions: create, list, get, update, pause, resume, run, delete.`,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|