@goodandready/dsh-cron 0.3.10 → 0.3.12
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 +24 -0
- package/README.ru.md +19 -0
- package/README.zh.md +13 -0
- package/lib/api-helpers.js +9 -9
- package/lib/api-task-webhook.js +247 -0
- package/lib/api.js +39 -16
- package/lib/cron-tool.js +1 -1
- package/lib/http-utils.js +128 -35
- package/lib/index.js +2 -2
- package/lib/nl-schedule.js +338 -0
- package/lib/prompt-interpolation.js +76 -4
- package/lib/routes.js +27 -19
- package/lib/scheduler.js +21 -8
- package/lib/store.js +6 -0
- package/lib/task-create.js +4 -0
- package/lib/task-patch.js +12 -0
- package/lib/task-transfer.js +5 -0
- package/package.json +1 -1
|
@@ -14,6 +14,36 @@ function formatTime(d) {
|
|
|
14
14
|
return `${hours}:${minutes}:${seconds}`;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Safely extract nested property from object by path.
|
|
19
|
+
* Supports dot and array bracket notation: "alerts[0].labels.container" or "issue.title".
|
|
20
|
+
*/
|
|
21
|
+
export function getNestedValue(obj, path) {
|
|
22
|
+
if (!obj || typeof obj !== 'object' || !path) return undefined;
|
|
23
|
+
// If property exists directly (e.g. headers with dashes like "x-github-event")
|
|
24
|
+
if (Object.prototype.hasOwnProperty.call(obj, path)) {
|
|
25
|
+
return obj[path];
|
|
26
|
+
}
|
|
27
|
+
// Try lowercase lookup for headers
|
|
28
|
+
const lowerPath = path.toLowerCase();
|
|
29
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
30
|
+
if (k.toLowerCase() === lowerPath) return v;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const normalized = path.replace(/\[(\w+)\]/g, '.$1');
|
|
34
|
+
const parts = normalized.split('.');
|
|
35
|
+
let current = obj;
|
|
36
|
+
for (const p of parts) {
|
|
37
|
+
if (current == null) return undefined;
|
|
38
|
+
if (typeof current === 'object' && Object.prototype.hasOwnProperty.call(current, p)) {
|
|
39
|
+
current = current[p];
|
|
40
|
+
} else {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return current;
|
|
45
|
+
}
|
|
46
|
+
|
|
17
47
|
/**
|
|
18
48
|
* Build dictionary of prompt interpolation variables for a task and run options.
|
|
19
49
|
*/
|
|
@@ -73,6 +103,16 @@ export function buildPromptContext(task = {}, options = {}) {
|
|
|
73
103
|
'task_title': task.title || '',
|
|
74
104
|
};
|
|
75
105
|
|
|
106
|
+
// Webhook context (#348)
|
|
107
|
+
if (options.webhook && typeof options.webhook === 'object') {
|
|
108
|
+
ctx.__webhook = options.webhook;
|
|
109
|
+
if (options.webhook.body !== undefined) {
|
|
110
|
+
ctx['webhook.body'] = typeof options.webhook.body === 'object'
|
|
111
|
+
? JSON.stringify(options.webhook.body)
|
|
112
|
+
: String(options.webhook.body);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
76
116
|
// DAG / Fan-In predecessor outputs (#328)
|
|
77
117
|
if (options.prevOutputs && typeof options.prevOutputs === 'object') {
|
|
78
118
|
for (const [k, v] of Object.entries(options.prevOutputs)) {
|
|
@@ -90,14 +130,17 @@ export function buildPromptContext(task = {}, options = {}) {
|
|
|
90
130
|
|
|
91
131
|
/**
|
|
92
132
|
* Replace {{var}} or {var} placeholders with values from context.
|
|
93
|
-
* Supports dot notation like {{prev.output}}
|
|
133
|
+
* Supports dot notation like {{prev.output}}, bracket notation like {{prevOutputs['step1']}},
|
|
134
|
+
* and nested webhook properties like {{webhook.body.alerts[0].labels.container}} (#348).
|
|
94
135
|
*/
|
|
95
136
|
export function interpolateTaskPrompt(text, context = {}) {
|
|
96
137
|
if (typeof text !== 'string') return text;
|
|
97
138
|
if (!text) return '';
|
|
98
139
|
|
|
99
|
-
|
|
100
|
-
|
|
140
|
+
let result = text;
|
|
141
|
+
|
|
142
|
+
// 1. Bracket notation for DAG: {{prevOutputs['step1']}} or {prevOutputs['step1']}
|
|
143
|
+
result = result.replace(/\{\{?prevOutputs\[['"]([^'"]+)['"]\]\}?\}/g, (match, stepId) => {
|
|
101
144
|
const key = `prevOutputs.${stepId}`;
|
|
102
145
|
if (Object.prototype.hasOwnProperty.call(context, key)) {
|
|
103
146
|
return context[key];
|
|
@@ -105,7 +148,21 @@ export function interpolateTaskPrompt(text, context = {}) {
|
|
|
105
148
|
return match;
|
|
106
149
|
});
|
|
107
150
|
|
|
108
|
-
// 2.
|
|
151
|
+
// 2. Webhook payload interpolation (#348): {webhook.body.path}, {webhook.headers.key}, {webhook.query.param}
|
|
152
|
+
if (context.__webhook && typeof context.__webhook === 'object') {
|
|
153
|
+
result = result.replace(/\{\{?webhook\.(body|headers|query)(?:\.([a-zA-Z0-9_.\[\]-]+))?\}?\}/g, (match, source, path) => {
|
|
154
|
+
const srcObj = context.__webhook[source];
|
|
155
|
+
if (!path) {
|
|
156
|
+
if (srcObj === undefined) return '';
|
|
157
|
+
return typeof srcObj === 'object' ? JSON.stringify(srcObj) : String(srcObj);
|
|
158
|
+
}
|
|
159
|
+
const val = getNestedValue(srcObj, path);
|
|
160
|
+
if (val === undefined) return '';
|
|
161
|
+
return typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 3. Standard {{var}} and {var}
|
|
109
166
|
result = result.replace(/\{\{([a-zA-Z0-9_.]+)\}\}/g, (match, varName) => {
|
|
110
167
|
if (Object.prototype.hasOwnProperty.call(context, varName)) {
|
|
111
168
|
const val = context[varName];
|
|
@@ -143,5 +200,20 @@ export function resolveTaskForExecution(task, options = {}) {
|
|
|
143
200
|
if (typeof resolved.url === 'string') {
|
|
144
201
|
resolved.url = interpolateTaskPrompt(resolved.url, context);
|
|
145
202
|
}
|
|
203
|
+
|
|
204
|
+
// Expose webhook payload in process environment for scripts (#348)
|
|
205
|
+
if (options.webhook && typeof options.webhook === 'object') {
|
|
206
|
+
const env = { ...(resolved.env || {}) };
|
|
207
|
+
if (options.webhook.body !== undefined) {
|
|
208
|
+
env.DSH_CRON_WEBHOOK_BODY = typeof options.webhook.body === 'object'
|
|
209
|
+
? JSON.stringify(options.webhook.body)
|
|
210
|
+
: String(options.webhook.body);
|
|
211
|
+
}
|
|
212
|
+
if (options.webhook.headers !== undefined) {
|
|
213
|
+
env.DSH_CRON_WEBHOOK_HEADERS = JSON.stringify(options.webhook.headers);
|
|
214
|
+
}
|
|
215
|
+
resolved.env = env;
|
|
216
|
+
}
|
|
217
|
+
|
|
146
218
|
return resolved;
|
|
147
219
|
}
|
package/lib/routes.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { handleTaskWebhookRoute } from './api-task-webhook.js';
|
|
1
2
|
import { calculateCostHeatmap } from './timeline.js';
|
|
2
3
|
import { buildAnalyticsDigest, formatDigestMarkdown, sendAnalyticsDigest } from './analytics-digest.js';
|
|
3
4
|
import { listVerifiedRecipes } from './recipes-catalog.js';
|
|
@@ -19,32 +20,39 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
|
19
20
|
ctx.effect(() => ctx.webServer.register({
|
|
20
21
|
kind: 'prefix',
|
|
21
22
|
path: '/dsh-cron/tasks',
|
|
22
|
-
handler: createCronApiHandler(store, scheduler, { recommendations })
|
|
23
|
+
handler: createCronApiHandler(store, scheduler, { recommendations, ctx })
|
|
23
24
|
}), 'dsh-cron: /tasks[...]');
|
|
24
25
|
|
|
25
26
|
// Alias route /dsh-cron/action/:id/:action for backward compatibility (#66)
|
|
26
27
|
ctx.effect(() => ctx.webServer.register({
|
|
27
28
|
kind: 'prefix',
|
|
28
29
|
path: '/dsh-cron/action',
|
|
29
|
-
handler: createCronApiHandler(store, scheduler,
|
|
30
|
+
handler: createCronApiHandler(store, scheduler, { ctx })
|
|
30
31
|
}), 'dsh-cron: /action/:id/:action');
|
|
31
32
|
|
|
32
33
|
ctx.effect(() => ctx.webServer.register({
|
|
33
34
|
kind: 'prefix',
|
|
34
35
|
path: '/dsh-cron/telegram',
|
|
35
|
-
handler: createCronApiHandler(store, scheduler,
|
|
36
|
+
handler: createCronApiHandler(store, scheduler, { ctx })
|
|
36
37
|
}), 'dsh-cron: /telegram[...]');
|
|
37
38
|
|
|
39
|
+
// Incoming HTTP Webhook triggers (#348)
|
|
40
|
+
ctx.effect(() => ctx.webServer.register({
|
|
41
|
+
kind: 'prefix',
|
|
42
|
+
path: '/dsh-cron/webhook',
|
|
43
|
+
handler: (req, res) => handleTaskWebhookRoute({ store, scheduler, req, res })
|
|
44
|
+
}), 'dsh-cron: /webhook[...]');
|
|
45
|
+
|
|
38
46
|
ctx.effect(() => ctx.webServer.register({
|
|
39
47
|
kind: 'prefix',
|
|
40
48
|
path: '/dsh-cron/heartbeat-ping',
|
|
41
|
-
handler: createCronApiHandler(store, scheduler,
|
|
49
|
+
handler: createCronApiHandler(store, scheduler, { ctx })
|
|
42
50
|
}), 'dsh-cron: /heartbeat-ping[...]');
|
|
43
51
|
|
|
44
52
|
ctx.effect(() => ctx.webServer.register({
|
|
45
53
|
kind: 'prefix',
|
|
46
54
|
path: '/dsh-cron/schedule',
|
|
47
|
-
handler: createCronApiHandler(store, scheduler,
|
|
55
|
+
handler: createCronApiHandler(store, scheduler, { ctx })
|
|
48
56
|
}), 'dsh-cron: /schedule[...]');
|
|
49
57
|
|
|
50
58
|
|
|
@@ -52,14 +60,14 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
|
52
60
|
ctx.effect(() => ctx.webServer.register({
|
|
53
61
|
kind: 'prefix',
|
|
54
62
|
path: '/dsh-cron/chains',
|
|
55
|
-
handler: (req, res) => handleChainRoutes(store, scheduler, req, res)
|
|
63
|
+
handler: (req, res) => handleChainRoutes(store, scheduler, req, res, ctx)
|
|
56
64
|
}), 'dsh-cron: /chains[...]');
|
|
57
65
|
|
|
58
66
|
// Run artifacts listing, download and safe preview (#321)
|
|
59
67
|
ctx.effect(() => ctx.webServer.register({
|
|
60
68
|
kind: 'prefix',
|
|
61
69
|
path: '/dsh-cron/runs',
|
|
62
|
-
handler: (req, res) => handleRunArtifactRoutes(store, req, res)
|
|
70
|
+
handler: (req, res) => handleRunArtifactRoutes(store, req, res, ctx)
|
|
63
71
|
}), 'dsh-cron: /runs[...]');
|
|
64
72
|
|
|
65
73
|
// GET /dsh-cron/metrics (#53) — Prometheus text exposition
|
|
@@ -90,7 +98,7 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
|
90
98
|
handler: async (req, res) => {
|
|
91
99
|
const settings = store && typeof store.getSettings === 'function' ? store.getSettings() : {};
|
|
92
100
|
const apiToken = settings.apiToken;
|
|
93
|
-
if (rejectCrossOrigin(req, res, { apiToken })) return;
|
|
101
|
+
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
94
102
|
await getModelsHandler(ctx, req, res, sendJson);
|
|
95
103
|
}
|
|
96
104
|
}), 'dsh-cron: /models');
|
|
@@ -100,7 +108,7 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
|
100
108
|
kind: 'exact',
|
|
101
109
|
path: '/dsh-cron/chat/start',
|
|
102
110
|
handler: async (req, res) => {
|
|
103
|
-
if (req.method === 'POST' && rejectCrossOrigin(req, res)) return;
|
|
111
|
+
if (req.method === 'POST' && rejectCrossOrigin(req, res, { ctx })) return;
|
|
104
112
|
await chatStartHandler(ctx, req, res, parseJsonBody, sendJson, randomUUID);
|
|
105
113
|
}
|
|
106
114
|
}), 'dsh-cron: /chat/start');
|
|
@@ -113,7 +121,7 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
|
113
121
|
try {
|
|
114
122
|
const settings = store && typeof store.getSettings === 'function' ? store.getSettings() : {};
|
|
115
123
|
const apiToken = settings.apiToken;
|
|
116
|
-
if (rejectCrossOrigin(req, res, { apiToken })) return;
|
|
124
|
+
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
117
125
|
|
|
118
126
|
const url = new URL(req.url, 'http://127.0.0.1');
|
|
119
127
|
const path = url.pathname;
|
|
@@ -161,7 +169,7 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
|
161
169
|
}
|
|
162
170
|
|
|
163
171
|
if (req.method === 'POST') {
|
|
164
|
-
if (rejectCrossOrigin(req, res)) return;
|
|
172
|
+
if (rejectCrossOrigin(req, res, { ctx })) return;
|
|
165
173
|
const { body } = await readBody(req, res);
|
|
166
174
|
const days = parseInt(body?.days || url.searchParams.get('days') || '7', 10);
|
|
167
175
|
const tasks = store.list ? store.list() : Object.values(store.tasks || {});
|
|
@@ -219,7 +227,7 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
|
219
227
|
}
|
|
220
228
|
const settings = store.getSettings ? store.getSettings() : {};
|
|
221
229
|
const apiToken = settings.apiToken;
|
|
222
|
-
if (rejectCrossOrigin(req, res, { apiToken })) return;
|
|
230
|
+
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
223
231
|
const categories = recipesByCategory();
|
|
224
232
|
const recipes = listVerifiedRecipes(settings);
|
|
225
233
|
sendJson(res, 200, { ok: true, categories, recipes });
|
|
@@ -241,7 +249,7 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
|
241
249
|
}
|
|
242
250
|
const settings = store.getSettings ? store.getSettings() : {};
|
|
243
251
|
const apiToken = settings.apiToken;
|
|
244
|
-
if (rejectCrossOrigin(req, res, { apiToken })) return;
|
|
252
|
+
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
245
253
|
|
|
246
254
|
const tasks = store.list({ status: 'active' });
|
|
247
255
|
const lastRuns = tasks.map((t) => t.lastRunAt || 0);
|
|
@@ -268,7 +276,7 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
|
268
276
|
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
269
277
|
return;
|
|
270
278
|
}
|
|
271
|
-
if (rejectCrossOrigin(req, res)) return;
|
|
279
|
+
if (rejectCrossOrigin(req, res, { ctx })) return;
|
|
272
280
|
const { body, error } = await readBody(req, res);
|
|
273
281
|
if (error) return;
|
|
274
282
|
const settings = store.getSettings();
|
|
@@ -300,7 +308,7 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
|
300
308
|
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
301
309
|
return;
|
|
302
310
|
}
|
|
303
|
-
if (rejectCrossOrigin(req, res)) return;
|
|
311
|
+
if (rejectCrossOrigin(req, res, { ctx })) return;
|
|
304
312
|
const { body, error } = await readBody(req, res);
|
|
305
313
|
if (error) return;
|
|
306
314
|
const settings = store.getSettings();
|
|
@@ -331,10 +339,10 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
|
331
339
|
/**
|
|
332
340
|
* Handler for chain step retries (#319)
|
|
333
341
|
*/
|
|
334
|
-
export async function handleChainRoutes(store, scheduler, req, res) {
|
|
342
|
+
export async function handleChainRoutes(store, scheduler, req, res, ctx) {
|
|
335
343
|
const settings = store && typeof store.getSettings === 'function' ? store.getSettings() : {};
|
|
336
344
|
const apiToken = settings.apiToken;
|
|
337
|
-
if (rejectCrossOrigin(req, res, { apiToken })) return;
|
|
345
|
+
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
338
346
|
const url = new URL(req.url, 'http://localhost');
|
|
339
347
|
|
|
340
348
|
// GET /dsh-cron/chains/:chainRunId (#334)
|
|
@@ -378,7 +386,7 @@ export async function handleChainRoutes(store, scheduler, req, res) {
|
|
|
378
386
|
/**
|
|
379
387
|
* Handler for run artifacts (#321)
|
|
380
388
|
*/
|
|
381
|
-
export async function handleRunArtifactRoutes(store, req, res) {
|
|
389
|
+
export async function handleRunArtifactRoutes(store, req, res, ctx) {
|
|
382
390
|
const url = new URL(req.url, 'http://localhost');
|
|
383
391
|
const m = url.pathname.match(/^\/dsh-cron\/runs\/([^\/]+)\/artifacts(?:\/([^\/]+)(?:\/(preview))?)?$/);
|
|
384
392
|
if (!m) {
|
|
@@ -391,7 +399,7 @@ export async function handleRunArtifactRoutes(store, req, res) {
|
|
|
391
399
|
}
|
|
392
400
|
const settings = store && typeof store.getSettings === 'function' ? store.getSettings() : {};
|
|
393
401
|
const apiToken = settings.apiToken;
|
|
394
|
-
if (rejectCrossOrigin(req, res, { apiToken })) return;
|
|
402
|
+
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
395
403
|
|
|
396
404
|
const [, runId, artifactId, action] = m;
|
|
397
405
|
if (!artifactId) {
|
package/lib/scheduler.js
CHANGED
|
@@ -2,6 +2,7 @@ import { logger } from './logger.js';
|
|
|
2
2
|
import { bestEffort } from './best-effort.js';
|
|
3
3
|
import { Cron } from 'croner';
|
|
4
4
|
import { FileSystemWatcherManager } from './watcher.js';
|
|
5
|
+
import { parseNaturalSchedule } from './nl-schedule.js';
|
|
5
6
|
|
|
6
7
|
const MAX_TIMEOUT_MS = 2147483647; // 2^31 - 1 ms (~24.85 days)
|
|
7
8
|
import {
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
} from './scheduler-lifecycle.js';
|
|
32
33
|
|
|
33
34
|
export {
|
|
35
|
+
parseNaturalSchedule,
|
|
34
36
|
executePreflight,
|
|
35
37
|
AGENT_TASK_TYPES,
|
|
36
38
|
addUsage,
|
|
@@ -212,25 +214,35 @@ function parseIntervalExpression(str) {
|
|
|
212
214
|
* @-shorthands (#15) and the friendly aliases, including the Russian input
|
|
213
215
|
* aliases. Russian keywords are accepted input, not display strings.
|
|
214
216
|
*/
|
|
217
|
+
function makeAliasResult(cronPattern, humanText) {
|
|
218
|
+
try {
|
|
219
|
+
const job = new Cron(cronPattern, { sloppyRanges: true });
|
|
220
|
+
const next = job.nextRun();
|
|
221
|
+
return { cronPattern, humanText, nextRun: next ? next.getTime() : null };
|
|
222
|
+
} catch {
|
|
223
|
+
return { cronPattern, humanText };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
215
227
|
function parseAliasExpression(str) {
|
|
216
228
|
const lower = str.toLowerCase();
|
|
217
|
-
if (lower === '@hourly') return
|
|
218
|
-
if (lower === '@daily' || lower === '@midnight') return
|
|
219
|
-
if (lower === '@weekly') return
|
|
220
|
-
if (lower === '@monthly') return
|
|
221
|
-
if (lower === '@yearly' || lower === '@annually') return
|
|
229
|
+
if (lower === '@hourly') return makeAliasResult('0 * * * *', 'Every hour');
|
|
230
|
+
if (lower === '@daily' || lower === '@midnight') return makeAliasResult('0 0 * * *', 'Every day at 00:00');
|
|
231
|
+
if (lower === '@weekly') return makeAliasResult('0 0 * * 0', 'Every week on Sunday at 00:00');
|
|
232
|
+
if (lower === '@monthly') return makeAliasResult('0 0 1 * *', 'Every month on the 1st at 00:00');
|
|
233
|
+
if (lower === '@yearly' || lower === '@annually') return makeAliasResult('0 0 1 1 *', 'Every year on Jan 1 at 00:00');
|
|
222
234
|
const everyAlias = str.match(/^@every\s+(\d+)\s*(sec|seconds?|m|min|minutes?|h|hr|hours?|d|days?)$/i);
|
|
223
235
|
if (everyAlias) {
|
|
224
236
|
return parseIntervalExpression(`every ${everyAlias[1]} ${everyAlias[2]}`);
|
|
225
237
|
}
|
|
226
238
|
if (lower === 'daily' || lower === 'каждый день') {
|
|
227
|
-
return
|
|
239
|
+
return makeAliasResult('0 9 * * *', 'Every day at 09:00');
|
|
228
240
|
}
|
|
229
241
|
if (lower === 'weekdays' || lower === 'в будние дни' || lower === 'по будням') {
|
|
230
|
-
return
|
|
242
|
+
return makeAliasResult('0 9 * * 1-5', 'Weekdays at 09:00');
|
|
231
243
|
}
|
|
232
244
|
if (lower === 'hourly' || lower === 'каждый час') {
|
|
233
|
-
return
|
|
245
|
+
return makeAliasResult('0 * * * *', 'Every hour');
|
|
234
246
|
}
|
|
235
247
|
return null;
|
|
236
248
|
}
|
|
@@ -261,6 +273,7 @@ export function parseScheduleExpression(input) {
|
|
|
261
273
|
|| parseRelativeOneShot(str)
|
|
262
274
|
|| parseIntervalExpression(str)
|
|
263
275
|
|| parseAliasExpression(str)
|
|
276
|
+
|| parseNaturalSchedule(str)
|
|
264
277
|
|| parseCronExpression(str);
|
|
265
278
|
}
|
|
266
279
|
|
package/lib/store.js
CHANGED
|
@@ -629,6 +629,12 @@ export class TaskStore {
|
|
|
629
629
|
targetSessionId: task.targetSessionId !== undefined ? String(task.targetSessionId).trim() : (prev.targetSessionId || ''),
|
|
630
630
|
targetSessionReset: task.targetSessionReset !== undefined ? String(task.targetSessionReset).trim() : (prev.targetSessionReset || 'never'),
|
|
631
631
|
sessionTitle: task.sessionTitle !== undefined ? String(task.sessionTitle).trim() : (prev.sessionTitle || ''),
|
|
632
|
+
webhookEnabled: task.webhookEnabled !== undefined
|
|
633
|
+
? Boolean(task.webhookEnabled)
|
|
634
|
+
: (prev.webhookEnabled !== undefined ? Boolean(prev.webhookEnabled) : Boolean(task.webhookTrigger?.enabled || prev.webhookTrigger?.enabled || false)),
|
|
635
|
+
webhookToken: task.webhookToken !== undefined ? String(task.webhookToken).trim() : (prev.webhookToken || (task.webhookTrigger?.token ? String(task.webhookTrigger.token).trim() : '')),
|
|
636
|
+
webhookSecret: task.webhookSecret !== undefined ? String(task.webhookSecret).trim() : (prev.webhookSecret || (task.webhookTrigger?.secret ? String(task.webhookTrigger.secret).trim() : '')),
|
|
637
|
+
webhookDebounceMs: task.webhookDebounceMs !== undefined ? Number(task.webhookDebounceMs) : (prev.webhookDebounceMs !== undefined ? Number(prev.webhookDebounceMs) : (Number(task.webhookTrigger?.debounceMs || prev.webhookTrigger?.debounceMs) || 0)),
|
|
632
638
|
totalTokens: task.totalTokens !== undefined ? Number(task.totalTokens) : (prev.totalTokens || 0),
|
|
633
639
|
totalCostUsd: task.totalCostUsd !== undefined ? Number(task.totalCostUsd) : (prev.totalCostUsd || 0),
|
|
634
640
|
createdAt: prev.createdAt || task.createdAt || now,
|
package/lib/task-create.js
CHANGED
|
@@ -49,6 +49,10 @@ export function executeCreateTask(store, scheduler, args) {
|
|
|
49
49
|
targetSessionId: args.targetSessionId ? String(args.targetSessionId).trim() : '',
|
|
50
50
|
targetSessionReset: args.targetSessionReset ? String(args.targetSessionReset).trim() : 'never',
|
|
51
51
|
sessionTitle: args.sessionTitle ? String(args.sessionTitle).trim() : '',
|
|
52
|
+
webhookEnabled: args.webhookEnabled !== undefined ? Boolean(args.webhookEnabled) : (args.webhookTrigger?.enabled !== undefined ? Boolean(args.webhookTrigger.enabled) : false),
|
|
53
|
+
webhookToken: args.webhookToken ? String(args.webhookToken).trim() : (args.webhookTrigger?.token ? String(args.webhookTrigger.token).trim() : undefined),
|
|
54
|
+
webhookSecret: args.webhookSecret ? String(args.webhookSecret).trim() : (args.webhookTrigger?.secret ? String(args.webhookTrigger.secret).trim() : undefined),
|
|
55
|
+
webhookDebounceMs: args.webhookDebounceMs !== undefined ? Number(args.webhookDebounceMs) : (args.webhookTrigger?.debounceMs !== undefined ? Number(args.webhookTrigger.debounceMs) : undefined),
|
|
52
56
|
onSuccess: args.onSuccess ? String(args.onSuccess).trim() : '',
|
|
53
57
|
onFailure: args.onFailure ? String(args.onFailure).trim() : '',
|
|
54
58
|
dependsOn: args.dependsOn !== undefined ? sanitizeDependsOn(args.dependsOn) : undefined,
|
package/lib/task-patch.js
CHANGED
|
@@ -91,6 +91,18 @@ export function buildTaskPatch(current, body) {
|
|
|
91
91
|
return { ok: false, error: err.message };
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
if (patch.webhookEnabled !== undefined) {
|
|
95
|
+
patch.webhookEnabled = Boolean(patch.webhookEnabled);
|
|
96
|
+
}
|
|
97
|
+
if (patch.webhookToken !== undefined) {
|
|
98
|
+
patch.webhookToken = String(patch.webhookToken).trim();
|
|
99
|
+
}
|
|
100
|
+
if (patch.webhookSecret !== undefined) {
|
|
101
|
+
patch.webhookSecret = String(patch.webhookSecret).trim();
|
|
102
|
+
}
|
|
103
|
+
if (patch.webhookDebounceMs !== undefined) {
|
|
104
|
+
patch.webhookDebounceMs = Math.max(0, Number(patch.webhookDebounceMs) || 0);
|
|
105
|
+
}
|
|
94
106
|
if (patch.requireApproval !== undefined) {
|
|
95
107
|
patch.requireApproval = Boolean(patch.requireApproval);
|
|
96
108
|
}
|
package/lib/task-transfer.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cron",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"description": "Background automation runner for DSH: isolated agent runs, script/HTTP/SSH/Docker runtimes, cost guard, notifications, heartbeats.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|