@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.
@@ -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}} and bracket notation like {{prevOutputs['step1']}}.
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
- // 1. Bracket notation: {{prevOutputs['step1']}} or {prevOutputs['step1']} or with double quotes
100
- let result = text.replace(/\{\{?prevOutputs\[['"]([^'"]+)['"]\]\}?\}/g, (match, stepId) => {
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. Standard {{var}} and {var}
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, null)
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, null)
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, null)
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, null)
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 { cronPattern: '0 * * * *', humanText: 'Every hour' };
218
- if (lower === '@daily' || lower === '@midnight') return { cronPattern: '0 0 * * *', humanText: 'Every day at 00:00' };
219
- if (lower === '@weekly') return { cronPattern: '0 0 * * 0', humanText: 'Every week on Sunday at 00:00' };
220
- if (lower === '@monthly') return { cronPattern: '0 0 1 * *', humanText: 'Every month on the 1st at 00:00' };
221
- if (lower === '@yearly' || lower === '@annually') return { cronPattern: '0 0 1 1 *', humanText: 'Every year on Jan 1 at 00:00' };
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 { cronPattern: '0 9 * * *', humanText: 'Every day at 09:00' };
239
+ return makeAliasResult('0 9 * * *', 'Every day at 09:00');
228
240
  }
229
241
  if (lower === 'weekdays' || lower === 'в будние дни' || lower === 'по будням') {
230
- return { cronPattern: '0 9 * * 1-5', humanText: 'Weekdays at 09:00' };
242
+ return makeAliasResult('0 9 * * 1-5', 'Weekdays at 09:00');
231
243
  }
232
244
  if (lower === 'hourly' || lower === 'каждый час') {
233
- return { cronPattern: '0 * * * *', humanText: 'Every hour' };
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,
@@ -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
  }
@@ -34,6 +34,11 @@ export const PATCHABLE_TASK_FIELDS = [
34
34
  'targetSessionId',
35
35
  'targetSessionReset',
36
36
  'sessionTitle',
37
+ 'webhookEnabled',
38
+ 'webhookToken',
39
+ 'webhookSecret',
40
+ 'webhookDebounceMs',
41
+ 'webhookTrigger',
37
42
  'env',
38
43
  'cwd',
39
44
  'workspaceId',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cron",
3
- "version": "0.3.10",
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",