@goodandready/dsh-cron 0.3.24 → 0.3.26

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.
Files changed (84) hide show
  1. package/lib/api-actions.js +1 -317
  2. package/lib/api-collection.js +1 -250
  3. package/lib/api-helpers.js +1 -100
  4. package/lib/api-import-export.js +1 -155
  5. package/lib/api-item.js +1 -190
  6. package/lib/api-task-webhook.js +1 -247
  7. package/lib/api-webhook.js +1 -183
  8. package/lib/api.js +1 -178
  9. package/lib/artifacts.js +1 -200
  10. package/lib/chain-checkpoints.js +1 -223
  11. package/lib/client-src/00-open.js +62 -0
  12. package/lib/client-src/10-locales.js +484 -0
  13. package/lib/client-src/20-styles.js +64 -0
  14. package/lib/client-src/30-utils.js +248 -0
  15. package/lib/client-src/40-delivery-settings.js +96 -0
  16. package/lib/client-src/45-dag-graph-view.js +287 -0
  17. package/lib/client-src/46-live-logs-modal.js +108 -0
  18. package/lib/client-src/47-analytics-digest-modal.js +166 -0
  19. package/lib/client-src/48-timeline-heatmap-view.js +176 -0
  20. package/lib/client-src/50-cron-screen-state.js +447 -0
  21. package/lib/client-src/52-cron-screen-actions.js +330 -0
  22. package/lib/client-src/53-cron-screen-modal-actions.js +640 -0
  23. package/lib/client-src/54-cron-screen-analytics-actions.js +68 -0
  24. package/lib/client-src/55-cron-screen-view.js +378 -0
  25. package/lib/client-src/60-modal-task-form-head.js +580 -0
  26. package/lib/client-src/62-modal-task-form-tail.js +466 -0
  27. package/lib/client-src/65-modal-dialogs.js +350 -0
  28. package/lib/client-src/70-settings-card.js +420 -0
  29. package/lib/client-src/80-sidebar-jobs.js +200 -0
  30. package/lib/client-src/90-close.js +200 -0
  31. package/lib/client.js +48 -3
  32. package/lib/commands.js +200 -0
  33. package/lib/durable-journal.js +1 -188
  34. package/lib/external-api.js +1 -74
  35. package/lib/index.js +17 -0
  36. package/lib/log-streamer.js +10 -2
  37. package/lib/metrics.js +1 -125
  38. package/lib/routes/api-actions.js +317 -0
  39. package/lib/routes/api-collection.js +250 -0
  40. package/lib/routes/api-helpers.js +100 -0
  41. package/lib/routes/api-import-export.js +155 -0
  42. package/lib/routes/api-item.js +191 -0
  43. package/lib/routes/api-task-webhook.js +247 -0
  44. package/lib/routes/api-webhook.js +183 -0
  45. package/lib/routes/api.js +178 -0
  46. package/lib/routes/external-api.js +74 -0
  47. package/lib/routes/index.js +11 -0
  48. package/lib/routes/metrics.js +125 -0
  49. package/lib/routes/routes.js +549 -0
  50. package/lib/routes.js +1 -549
  51. package/lib/runner-external.js +1 -352
  52. package/lib/runner-worktree.js +1 -90
  53. package/lib/runner.js +1 -881
  54. package/lib/runtimes/index.js +4 -0
  55. package/lib/runtimes/runner-external.js +352 -0
  56. package/lib/runtimes/runner-worktree.js +90 -0
  57. package/lib/runtimes/runner.js +881 -0
  58. package/lib/runtimes/runtimes.js +198 -0
  59. package/lib/runtimes.js +1 -198
  60. package/lib/scheduler/index.js +6 -0
  61. package/lib/scheduler/scheduler-assists.js +142 -0
  62. package/lib/scheduler/scheduler-completion.js +417 -0
  63. package/lib/scheduler/scheduler-execution.js +360 -0
  64. package/lib/scheduler/scheduler-lifecycle.js +157 -0
  65. package/lib/scheduler/scheduler-preflight.js +195 -0
  66. package/lib/scheduler/scheduler.js +751 -0
  67. package/lib/scheduler-assists.js +1 -142
  68. package/lib/scheduler-completion.js +1 -417
  69. package/lib/scheduler-execution.js +1 -360
  70. package/lib/scheduler-lifecycle.js +1 -157
  71. package/lib/scheduler-preflight.js +1 -195
  72. package/lib/scheduler.js +1 -751
  73. package/lib/store/artifacts.js +200 -0
  74. package/lib/store/chain-checkpoints.js +223 -0
  75. package/lib/store/durable-journal.js +188 -0
  76. package/lib/store/index.js +6 -0
  77. package/lib/store/store.js +1179 -0
  78. package/lib/store/task-schema.js +180 -0
  79. package/lib/store/task-transfer.js +310 -0
  80. package/lib/store.js +2 -1179
  81. package/lib/task-schema.js +1 -180
  82. package/lib/task-transfer.js +1 -310
  83. package/lib/tui-settings.js +157 -0
  84. package/package.json +6 -3
@@ -1,317 +1 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { captureGitDiff, discardGitChanges, applyGitDiff } from './git-diff.js';
3
- import { applyDoctorFix, revertDoctorFix, diagnoseFailure } from './doctor.js';
4
- import { rollbackPromptVersion } from './prompt-evolver.js';
5
- import { approvalManager } from './approval.js';
6
- import { MANAGED_BY_CONFIG } from './config-jobs.js';
7
- import { sendJson, readBody, redactTaskSecrets } from './http-utils.js';
8
- import { buildDuplicateTask } from './task-transfer.js';
9
- import { refuseConfigOwned, NOT_FOUND } from './api-collection.js';
10
-
11
- export async function handleItemPost({ store, scheduler, req, res, url, id, action, subAction }) {
12
- if (action === 'git-diff' && (subAction === 'discard' || subAction === 'revert')) {
13
- const task = store.get(id);
14
- if (!task) {
15
- sendJson(res, 404, NOT_FOUND);
16
- return true;
17
- }
18
- const targetDir = task.cwd;
19
- if (!targetDir) {
20
- sendJson(res, 400, { ok: false, error: 'Task does not define a working directory (cwd)' });
21
- return true;
22
- }
23
- const result = await discardGitChanges(targetDir);
24
- sendJson(res, result.ok ? 200 : 500, result);
25
- return true;
26
- }
27
-
28
- if (action === 'git-diff' && subAction === 'apply') {
29
- const task = store.get(id);
30
- if (!task) {
31
- sendJson(res, 404, NOT_FOUND);
32
- return true;
33
- }
34
- const body = await readBody(req, res);
35
- const bodyData = (body && body.body) || {};
36
- let patch = bodyData.diff || bodyData.patch;
37
- if (!patch) {
38
- const runs = store.getHistory(id, 5);
39
- const targetRun = runs.find((r) => r.gitDiff?.diff);
40
- patch = targetRun?.gitDiff?.diff;
41
- }
42
- if (!patch) {
43
- sendJson(res, 400, { ok: false, error: 'No diff patch provided or found in run history' });
44
- return true;
45
- }
46
- const targetDir = task.cwd;
47
- if (!targetDir) {
48
- sendJson(res, 400, { ok: false, error: 'Task does not define a working directory (cwd)' });
49
- return true;
50
- }
51
- const result = await applyGitDiff(targetDir, patch);
52
- sendJson(res, result.ok ? 200 : 500, result);
53
- return true;
54
- }
55
-
56
- if ((action === 'doctor' && subAction === 'apply') || action === 'doctor-apply') {
57
- const task = store.get(id);
58
- if (!task) {
59
- sendJson(res, 404, NOT_FOUND);
60
- return true;
61
- }
62
- const body = await readBody(req, res);
63
- const bodyData = (body && body.body) || {};
64
- let fix = bodyData.fix || bodyData.doctorReport;
65
- if (!fix) {
66
- const runs = store.getHistory(id, 5);
67
- const reportRun = runs.find((r) => r.doctorReport);
68
- fix = reportRun?.doctorReport;
69
- }
70
- if (!fix || !fix.suggestedFix) {
71
- sendJson(res, 400, { ok: false, error: 'No doctor fix found to apply' });
72
- return true;
73
- }
74
- try {
75
- const result = await applyDoctorFix(task, fix, { store, user: bodyData.user || 'operator' });
76
- if (!result.ok) {
77
- sendJson(res, 400, { ok: false, error: result.error });
78
- return true;
79
- }
80
- sendJson(res, 200, {
81
- ok: true,
82
- taskId: id,
83
- task: redactTaskSecrets(task),
84
- revision: result.revision,
85
- });
86
- } catch (err) {
87
- sendJson(res, 500, { ok: false, error: err.message });
88
- }
89
- return true;
90
- }
91
-
92
- if ((action === 'doctor' && subAction === 'revert') || action === 'doctor-revert') {
93
- const task = store.get(id);
94
- if (!task) {
95
- sendJson(res, 404, NOT_FOUND);
96
- return true;
97
- }
98
- const body = await readBody(req, res);
99
- const bodyData = (body && body.body) || {};
100
- try {
101
- const result = await revertDoctorFix(task, { store, user: bodyData.user || 'operator' });
102
- if (!result.ok) {
103
- sendJson(res, 400, { ok: false, error: result.error });
104
- return true;
105
- }
106
- sendJson(res, 200, {
107
- ok: true,
108
- taskId: id,
109
- task: redactTaskSecrets(task),
110
- revertedCode: result.revertedCode,
111
- });
112
- } catch (err) {
113
- sendJson(res, 500, { ok: false, error: err.message });
114
- }
115
- return true;
116
- }
117
-
118
- if ((action === 'doctor' && subAction === 'diagnose') || action === 'doctor-diagnose') {
119
- const task = store.get(id);
120
- if (!task) {
121
- sendJson(res, 404, NOT_FOUND);
122
- return true;
123
- }
124
- const body = await readBody(req, res);
125
- const bodyData = (body && body.body) || {};
126
- const runId = bodyData.runId;
127
- const runs = store.getHistory(id, 10);
128
- const targetRun = runId ? runs.find((r) => r.id === runId) : runs[0];
129
- if (!targetRun) {
130
- sendJson(res, 404, { ok: false, error: 'No run history found to diagnose' });
131
- return true;
132
- }
133
- try {
134
- const doctorReport = await diagnoseFailure({
135
- task,
136
- runInfo: targetRun,
137
- ask: scheduler?.askModel,
138
- });
139
- targetRun.doctorReport = doctorReport;
140
- sendJson(res, 200, { ok: true, taskId: id, doctorReport });
141
- } catch (err) {
142
- sendJson(res, 500, { ok: false, error: err.message });
143
- }
144
- return true;
145
- }
146
- if (action === 'prompt-rollback') {
147
- const task = store.get(id);
148
- if (!task) {
149
- sendJson(res, 404, NOT_FOUND);
150
- return true;
151
- }
152
- const body = await readBodyJson(req).catch(() => ({}));
153
- const targetVersion = Number(body.version || url.searchParams.get('version'));
154
- if (!targetVersion || targetVersion < 1) {
155
- sendJson(res, 400, { ok: false, error: 'A valid target prompt version is required' });
156
- return true;
157
- }
158
- try {
159
- rollbackPromptVersion(task, targetVersion);
160
- store.set(task);
161
- sendJson(res, 200, {
162
- ok: true,
163
- task: redactTaskSecrets(task),
164
- promptVersion: task.promptVersion,
165
- promptHistory: task.promptHistory,
166
- });
167
- } catch (err) {
168
- sendJson(res, 400, { ok: false, error: err.message });
169
- }
170
- return true;
171
- }
172
- if (action === 'retry-delivery') {
173
- const task = store.get(id);
174
- if (!task) {
175
- sendJson(res, 404, NOT_FOUND);
176
- return true;
177
- }
178
- const body = await readBodyJson(req).catch(() => ({}));
179
- const runId = body.runId || url.searchParams.get('runId');
180
- const runs = store.history.get(id) || [];
181
- const run = runId ? runs.find(r => r.id === runId) : runs[0];
182
- if (!run) {
183
- sendJson(res, 404, { ok: false, error: 'Run not found' });
184
- return true;
185
- }
186
- const delivery = await scheduler.deliverNotifications(task, run);
187
- run.delivery = delivery;
188
- if (Array.isArray(run.stages)) {
189
- const deliveryStage = run.stages.find(s => s.name === 'delivery');
190
- if (deliveryStage) {
191
- deliveryStage.status = delivery?.status || 'delivered';
192
- deliveryStage.delivered = delivery?.delivered;
193
- deliveryStage.failures = delivery?.failures;
194
- }
195
- }
196
- store.scheduleSave();
197
- sendJson(res, 200, { ok: true, delivery, run });
198
- return true;
199
- }
200
- if (action === 'approve' || action === 'reject') {
201
- const task = store.get(id);
202
- if (!task) {
203
- sendJson(res, 404, NOT_FOUND);
204
- return true;
205
- }
206
- const body = await readBodyJson(req).catch(() => ({}));
207
- const proposalId = body.proposalId || url.searchParams.get('proposalId');
208
- const proposal = proposalId
209
- ? approvalManager.getProposal(proposalId)
210
- : approvalManager.getPendingProposalForTask(id);
211
-
212
- if (!proposal) {
213
- sendJson(res, 404, { ok: false, error: 'No pending approval proposal found for this task' });
214
- return true;
215
- }
216
-
217
- const decision = action === 'approve' ? 'approve' : 'reject';
218
- const result = approvalManager.decide(proposal.id, decision, {
219
- decidedBy: body.decidedBy || 'web-user',
220
- reason: body.reason || '',
221
- });
222
-
223
- if (!result.ok) {
224
- sendJson(res, 400, result);
225
- return true;
226
- }
227
-
228
- if (decision === 'approve') {
229
- scheduler.runTask(id, { approvedBy: body.decidedBy || 'web-user' }).catch(() => {});
230
- }
231
-
232
- sendJson(res, 200, { ok: true, proposal: result.proposal });
233
- return true;
234
- }
235
- if (action === 'resolve-uncertain') {
236
- const task = store.get(id);
237
- if (!task) {
238
- sendJson(res, 404, NOT_FOUND);
239
- return true;
240
- }
241
- const body = await readBody(req).then(b => JSON.parse(b || '{}')).catch(() => ({}));
242
- const runId = body.runId || url.searchParams.get('runId');
243
- if (!runId) {
244
- sendJson(res, 400, { ok: false, error: 'runId is required' });
245
- return true;
246
- }
247
- const result = store.durableJournal.resolveUncertain(id, runId, {
248
- decision: body.decision || 'mark_failed',
249
- note: body.note || '',
250
- resolvedBy: body.resolvedBy || 'user',
251
- });
252
- if (!result.ok) {
253
- sendJson(res, 400, result);
254
- return true;
255
- }
256
- sendJson(res, 200, result);
257
- return true;
258
- }
259
- if (action === 'run') {
260
- if (!store.get(id)) {
261
- sendJson(res, 404, NOT_FOUND);
262
- return true;
263
- }
264
- await scheduler.triggerManualRun(id);
265
- sendJson(res, 200, { ok: true, task: redactTaskSecrets(store.get(id)) });
266
- return true;
267
- }
268
- if (action === 'duplicate') {
269
- const source = store.get(id);
270
- if (!source) {
271
- sendJson(res, 404, NOT_FOUND);
272
- return true;
273
- }
274
- const copy = buildDuplicateTask(source, { id: randomUUID() });
275
- try {
276
- store.set(copy);
277
- } catch (err) {
278
- sendJson(res, 500, { ok: false, error: 'Failed to persist duplicate task: ' + err.message });
279
- return true;
280
- }
281
- // A copy starts paused: it must never fire on its own before the user
282
- // reviews the schedule (a duplicated one-shot may point at a past time).
283
- scheduler.pauseTask(copy.id);
284
- sendJson(res, 200, { ok: true, task: redactTaskSecrets(copy) });
285
- return true;
286
- }
287
- if (action === 'pause' || action === 'resume') {
288
- if (store.get(id) && store.get(id).managedBy === MANAGED_BY_CONFIG) {
289
- refuseConfigOwned(res, id);
290
- return true;
291
- }
292
- const task = action === 'pause' ? scheduler.pauseTask(id) : scheduler.resumeTask(id);
293
- if (!task) {
294
- sendJson(res, 404, NOT_FOUND);
295
- return true;
296
- }
297
- sendJson(res, 200, { ok: true, task: redactTaskSecrets(task) });
298
- return true;
299
- }
300
- if (action === 'toggle' || (!action && url.searchParams.get('action') === 'toggle')) {
301
- const stored = store.get(id);
302
- if (!stored) {
303
- sendJson(res, 404, NOT_FOUND);
304
- return true;
305
- }
306
- if (stored.managedBy === MANAGED_BY_CONFIG) {
307
- refuseConfigOwned(res, id);
308
- return true;
309
- }
310
- const task = scheduler.toggleTask(id);
311
- sendJson(res, 200, { ok: true, task: redactTaskSecrets(task) });
312
- return true;
313
- }
314
- return false;
315
- }
316
-
317
-
1
+ export * from './routes/api-actions.js';
@@ -1,250 +1 @@
1
- import { createHash } from 'node:crypto';
2
- import { parseScheduleExpression } from './scheduler.js';
3
- import { normalizeTaskType, CODE_EXECUTING_TYPES, EXTERNAL_RUNTIMES } from './runtimes.js';
4
- import { CHANNEL_IDS, unknownChannelIds } from './channels.js';
5
- import { MANAGED_BY_CONFIG, configOwnedMessage } from './config-jobs.js';
6
- import { sendJson, rejectCrossOrigin, readBody, SCRIPT_CONFIRM_HEADER, redactTaskSecrets, restoreTaskSecrets } from './http-utils.js';
7
- import {
8
- validateTaskType,
9
- RESERVED_TASK_IDS,
10
- isCodeExecutingTask,
11
- } from './task-transfer.js';
12
- import { sanitizeAdaptiveConfig } from './adaptive.js';
13
- import { sanitizeValidators } from './validators.js';
14
-
15
- export const NOT_FOUND = { ok: false, error: 'Task not found' };
16
- export const NOT_ALLOWED = { ok: false, error: 'Method not allowed' };
17
-
18
- /**
19
- * A task declared in the profile config is owned by that file (#50): the next
20
- * plugin start would revert any change made here, so every mutating route
21
- * refuses it and says where the task really lives.
22
- */
23
- export function refuseConfigOwned(res, id) {
24
- sendJson(res, 409, { ok: false, error: configOwnedMessage(id) });
25
- }
26
-
27
- // ------------------------------------------------------------- collection
28
-
29
- export async function handleTaskCollection({ store, scheduler, collection, req, res, url, scope, apiToken, ctx }) {
30
- // The alias family only exists for individual tasks.
31
- if (scope === 'action' || !collection) {
32
- sendJson(res, 405, NOT_ALLOWED);
33
- return;
34
- }
35
- if (req.method === 'GET') {
36
- if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
37
- listTasks({ store, scheduler, collection, req, res, url });
38
- return;
39
- }
40
- if (req.method === 'POST') {
41
- await createOrUpdateTask({ store, scheduler, req, res, apiToken, ctx: undefined });
42
- return;
43
- }
44
- sendJson(res, 405, NOT_ALLOWED);
45
- }
46
-
47
- export function listTasks({ store, scheduler, collection, req, res, url }) {
48
- const status = url.searchParams.get('status') || 'all';
49
- const query = url.searchParams.get('query') || '';
50
- const list = store.list({ status, query }).map((t) => ({
51
- ...redactTaskSecrets(t),
52
- running: scheduler.isRunning(t.id),
53
- runningSince: scheduler.runningSince(t.id),
54
- }));
55
- const stats = store.getAggregatedStats();
56
- const payload = { ok: true, tasks: list, recommendations: collection.recommendations, stats };
57
-
58
- // Calculate ETag based on payload JSON representation (#134)
59
- const bodyString = JSON.stringify(payload);
60
- const etag = 'W/"' + createHash('sha1').update(bodyString).digest('hex') + '"';
61
-
62
- if (req && req.headers) {
63
- const ifNoneMatch = req.headers['if-none-match'];
64
- if (ifNoneMatch && (ifNoneMatch === etag || ifNoneMatch === etag.slice(2))) {
65
- res.writeHead(304, { 'ETag': etag, 'Cache-Control': 'no-cache' });
66
- res.end();
67
- return;
68
- }
69
- }
70
-
71
- if (typeof res.setHeader === 'function') {
72
- res.setHeader('ETag', etag);
73
- res.setHeader('Cache-Control', 'no-cache');
74
- }
75
- sendJson(res, 200, payload);
76
- }
77
-
78
- export async function createOrUpdateTask({ store, scheduler, req, res, apiToken, ctx }) {
79
- if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
80
- const { body: rawBody, error } = await readBody(req, res);
81
- if (error) return;
82
- const existing = rawBody.id ? store.get(rawBody.id) : null;
83
- const body = existing ? restoreTaskSecrets(rawBody, existing) : rawBody;
84
- if (!body.title || !body.schedule || !body.prompt) {
85
- sendJson(res, 400, { ok: false, error: 'Fields title, schedule and prompt are required' });
86
- return;
87
- }
88
- const unknownChannels = unknownChannelIds(body.channels);
89
- if (unknownChannels.length) {
90
- sendJson(res, 400, {
91
- ok: false,
92
- error: 'Unknown channel ids: ' + unknownChannels.join(', '),
93
- unknownChannels,
94
- });
95
- return;
96
- }
97
- const taskType = normalizeTaskType(body.type);
98
- if ((CODE_EXECUTING_TYPES.includes(taskType) || isCodeExecutingTask(body)) && req.headers[SCRIPT_CONFIRM_HEADER] !== 'script') {
99
- sendJson(res, 403, { ok: false, error: `Creating code-executing tasks over HTTP requires the x-dsh-cron-confirm: script header` });
100
- return;
101
- }
102
- const typeError = validateTaskType(taskType, body);
103
- if (typeError) {
104
- sendJson(res, 400, { ok: false, error: typeError });
105
- return;
106
- }
107
- let parsed;
108
- try {
109
- parsed = parseScheduleExpression(body.schedule);
110
- } catch (err) {
111
- sendJson(res, 400, { ok: false, error: err.message });
112
- return;
113
- }
114
- // A client-supplied id must not shadow the collection routes under
115
- // /dsh-cron/tasks, otherwise that task could never be fetched or deleted
116
- // again (#42 review finding).
117
- if (body.id && RESERVED_TASK_IDS.includes(String(body.id))) {
118
- sendJson(res, 400, { ok: false, error: `Task id "${body.id}" is reserved` });
119
- return;
120
- }
121
- // Create-or-update with an existing id is an update too, so the config-owned
122
- // refusal must apply here as well — otherwise the POST route would be a way
123
- // around the guard on PATCH (#50 review finding).
124
- if (body.id) {
125
- const existing = store.get(body.id);
126
- if (existing && existing.managedBy === MANAGED_BY_CONFIG) {
127
- refuseConfigOwned(res, body.id);
128
- return;
129
- }
130
- }
131
- let task;
132
- try {
133
- task = store.set(buildTaskRecord(store, body, taskType, parsed));
134
- } catch (err) {
135
- sendJson(res, 500, { ok: false, error: 'Failed to persist task: ' + err.message });
136
- return;
137
- }
138
- if (task.status === 'active') {
139
- scheduler.scheduleTask(task);
140
- } else {
141
- scheduler.pauseTask(task.id);
142
- }
143
- sendJson(res, 200, { ok: true, task: redactTaskSecrets(task) });
144
- }
145
-
146
- /** Merge a create/update payload onto the stored task, if any. */
147
- export function buildTaskRecord(store, body, taskType, parsed) {
148
- const current = body.id ? store.get(body.id) : null;
149
- const resolvedStatus = body.status || (current ? current.status : 'active');
150
- return {
151
- id: body.id,
152
- title: body.title,
153
- schedule: parsed.cronPattern || body.schedule,
154
- scheduleText: body.scheduleText || parsed.humanText,
155
- prompt: body.prompt,
156
- type: taskType,
157
- delivery: body.delivery || 'current',
158
- status: resolvedStatus,
159
- provider: body.provider || undefined,
160
- model: body.model || undefined,
161
- fallbackProvider: body.fallbackProvider || undefined,
162
- fallbackModel: body.fallbackModel || undefined,
163
- notifyTelegram: body.notifyTelegram !== undefined ? Boolean(body.notifyTelegram) : (current ? current.notifyTelegram : undefined),
164
- onlyOnFailure: body.onlyOnFailure !== undefined ? Boolean(body.onlyOnFailure) : (current ? current.onlyOnFailure : undefined),
165
- timeoutSeconds: body.timeoutSeconds !== undefined ? Number(body.timeoutSeconds) : (current ? current.timeoutSeconds : 1800),
166
- overlapPolicy: body.overlapPolicy || (current ? current.overlapPolicy : 'skip'),
167
- kanbanMode: body.kanbanMode || (current ? current.kanbanMode : 'none'),
168
- timezone: body.timezone !== undefined ? String(body.timezone).trim() : (current ? current.timezone : ''),
169
- misfirePolicy: ['skip', 'runOnce', 'catchUpAll'].includes(body.misfirePolicy) ? body.misfirePolicy : (current ? (current.misfirePolicy || 'skip') : 'skip'),
170
- maxRetries: body.maxRetries !== undefined ? Math.max(0, Number(body.maxRetries) || 0) : (current ? (Number(current.maxRetries) || 0) : 0),
171
- retryBackoffMs: body.retryBackoffMs !== undefined ? Math.max(1000, Number(body.retryBackoffMs) || 30000) : (current ? (Number(current.retryBackoffMs) || 30000) : 30000),
172
- permissionPreset: !EXTERNAL_RUNTIMES.includes(taskType) && ['default', 'read-only', 'workspace-write', 'danger-full-access', 'full'].includes(body.permissionPreset) ? body.permissionPreset : (current && !EXTERNAL_RUNTIMES.includes(taskType) ? (current.permissionPreset || 'default') : 'default'),
173
- validators: Array.isArray(body.validators) ? sanitizeValidators(body.validators) : (current ? current.validators : undefined),
174
- ...mergeExecutionFields(current, body, parsed),
175
- };
176
- }
177
-
178
- // export/import logic moved to ./api-import-export.js
179
-
180
- /**
181
- * Runtime and execution settings of a task, merged onto the stored one. Split
182
- * out of buildTaskRecord so both stay readable.
183
- */
184
- export function mergeExecutionFields(current, body, parsed) {
185
- return {
186
- env: body.env && typeof body.env === 'object' ? body.env : (current ? current.env : undefined),
187
- cwd: body.cwd !== undefined ? String(body.cwd).trim() : (current ? current.cwd : ''),
188
- channels: Array.isArray(body.channels)
189
- ? body.channels.filter((c) => CHANNEL_IDS.includes(c))
190
- : (current ? current.channels : undefined),
191
- template: body.template !== undefined ? String(body.template) : (current ? current.template : ''),
192
- silentRule: body.silentRule !== undefined ? String(body.silentRule) : (current ? current.silentRule : ''),
193
- inspectOnFailure: body.inspectOnFailure !== undefined ? Boolean(body.inspectOnFailure) : (current ? Boolean(current.inspectOnFailure) : false),
194
- workspaceId: body.workspaceId !== undefined ? String(body.workspaceId).trim() : (current ? current.workspaceId : ''),
195
- worktree: body.worktree !== undefined ? Boolean(body.worktree) : (current ? Boolean(current.worktree) : false),
196
- keepWorktree: body.keepWorktree !== undefined ? Boolean(body.keepWorktree) : (current ? Boolean(current.keepWorktree) : false),
197
- captureGitDiff: body.captureGitDiff !== undefined ? Boolean(body.captureGitDiff) : (body.captureDiff !== undefined ? Boolean(body.captureDiff) : (current ? current.captureGitDiff : undefined)),
198
- httpMethod: body.httpMethod ? String(body.httpMethod).toUpperCase() : (current ? current.httpMethod : 'GET'),
199
- httpUrl: body.httpUrl !== undefined ? String(body.httpUrl).trim() : (current ? current.httpUrl : ''),
200
- httpHeaders: body.httpHeaders !== undefined ? body.httpHeaders : (current ? current.httpHeaders : undefined),
201
- httpBody: body.httpBody !== undefined ? String(body.httpBody) : (current ? current.httpBody : ''),
202
- sshProfileId: body.sshProfileId !== undefined ? String(body.sshProfileId).trim() : (current ? current.sshProfileId : ''),
203
- sshTarget: body.sshTarget !== undefined ? String(body.sshTarget).trim() : (current ? current.sshTarget : ''),
204
- sshPort: body.sshPort !== undefined ? Number(body.sshPort) || 0 : (current ? current.sshPort : 0),
205
- sshKeyPath: body.sshKeyPath !== undefined ? String(body.sshKeyPath).trim() : (current ? current.sshKeyPath : ''),
206
- dockerImage: body.dockerImage !== undefined ? String(body.dockerImage).trim() : (current ? current.dockerImage : ''),
207
- pythonPath: body.pythonPath !== undefined ? String(body.pythonPath).trim() : (current ? current.pythonPath : ''),
208
- nodePath: body.nodePath !== undefined ? String(body.nodePath).trim() : (current ? current.nodePath : ''),
209
- skillName: body.skillName !== undefined ? String(body.skillName).trim() : (current ? current.skillName : ''),
210
- workflowName: body.workflowName !== undefined ? String(body.workflowName).trim() : (current ? current.workflowName : ''),
211
- oneShot: Boolean((parsed && parsed.isOneShot) || (body && body.oneShot)),
212
- targetTimestamp: parsed && parsed.isOneShot
213
- ? (current && current.schedule === (parsed.cronPattern || body.schedule) ? (current.targetTimestamp || parsed.targetTimestamp) : parsed.targetTimestamp)
214
- : (current ? current.targetTimestamp : undefined),
215
- costLimitUsd: body.costLimitUsd !== undefined ? (body.costLimitUsd === null ? null : (Number(body.costLimitUsd) > 0 ? Number(body.costLimitUsd) : null)) : (current ? current.costLimitUsd : undefined),
216
- dailyCostLimitUsd: body.dailyCostLimitUsd !== undefined ? (body.dailyCostLimitUsd === null ? null : (Number(body.dailyCostLimitUsd) > 0 ? Number(body.dailyCostLimitUsd) : null)) : (current ? current.dailyCostLimitUsd : undefined),
217
- tokenLimit: body.tokenLimit !== undefined ? (body.tokenLimit === null ? null : (Number(body.tokenLimit) > 0 ? Number(body.tokenLimit) : null)) : (current ? current.tokenLimit : undefined),
218
- pausedReason: body.pausedReason !== undefined ? (body.pausedReason ? String(body.pausedReason) : null) : (current ? current.pausedReason : undefined),
219
- agentPreset: body.agentPreset !== undefined ? String(body.agentPreset).trim() : (current ? (current.agentPreset || '') : ''),
220
- targetSessionId: body.targetSessionId !== undefined ? String(body.targetSessionId).trim() : (current ? (current.targetSessionId || '') : ''),
221
- targetSessionReset: body.targetSessionReset !== undefined ? String(body.targetSessionReset).trim() : (current ? (current.targetSessionReset || 'never') : 'never'),
222
- sessionTitle: body.sessionTitle !== undefined ? String(body.sessionTitle).trim() : (current ? (current.sessionTitle || '') : ''),
223
- webhookEnabled: body.webhookEnabled !== undefined
224
- ? Boolean(body.webhookEnabled)
225
- : (body.webhookTrigger?.enabled !== undefined ? Boolean(body.webhookTrigger.enabled) : (current ? Boolean(current.webhookEnabled || current.webhookTrigger?.enabled) : false)),
226
- webhookToken: body.webhookToken !== undefined
227
- ? String(body.webhookToken).trim()
228
- : (body.webhookTrigger?.token ? String(body.webhookTrigger.token).trim() : (current ? (current.webhookToken || current.webhookTrigger?.token || '') : '')),
229
- webhookSecret: body.webhookSecret !== undefined
230
- ? String(body.webhookSecret).trim()
231
- : (body.webhookTrigger?.secret ? String(body.webhookTrigger.secret).trim() : (current ? (current.webhookSecret || current.webhookTrigger?.secret || '') : '')),
232
- webhookDebounceMs: body.webhookDebounceMs !== undefined
233
- ? (Number(body.webhookDebounceMs) || 0)
234
- : (body.webhookTrigger?.debounceMs !== undefined ? (Number(body.webhookTrigger.debounceMs) || 0) : (current ? (Number(current.webhookDebounceMs || current.webhookTrigger?.debounceMs) || 0) : 0)),
235
- onSuccess: body.onSuccess !== undefined ? String(body.onSuccess).trim() : (current ? (current.onSuccess || '') : ''),
236
- onFailure: body.onFailure !== undefined ? String(body.onFailure).trim() : (current ? (current.onFailure || '') : ''),
237
- heartbeatIntervalSeconds: body.heartbeatIntervalSeconds !== undefined ? Number(body.heartbeatIntervalSeconds) : (current ? (Number(current.heartbeatIntervalSeconds) || 0) : 0),
238
- gracePeriodSeconds: body.gracePeriodSeconds !== undefined ? Number(body.gracePeriodSeconds) : (current ? (Number(current.gracePeriodSeconds) || 300) : 300),
239
- preflightType: body.preflightType !== undefined ? String(body.preflightType).trim().toLowerCase() : (current ? (current.preflightType || 'none') : 'none'),
240
- preflightTarget: body.preflightTarget !== undefined ? String(body.preflightTarget).trim() : (current ? (current.preflightTarget || '') : ''),
241
- priority: body.priority !== undefined ? (Number(body.priority) || 5) : (current ? (Number(current.priority) || 5) : 5),
242
- concurrencyGroup: body.concurrencyGroup !== undefined ? String(body.concurrencyGroup).trim() : (current ? (current.concurrencyGroup || 'default') : 'default'),
243
- selfHealingCommand: body.selfHealingCommand !== undefined ? String(body.selfHealingCommand).trim() : (current ? (current.selfHealingCommand || '') : ''),
244
- autoDiagnose: body.autoDiagnose !== undefined ? Boolean(body.autoDiagnose) : (current ? Boolean(current.autoDiagnose) : false),
245
- adaptive: body.adaptive !== undefined ? sanitizeAdaptiveConfig(body.adaptive) : (current ? current.adaptive : undefined),
246
- adaptiveState: current ? current.adaptiveState : undefined,
247
- };
248
- }
249
-
250
-
1
+ export * from './routes/api-collection.js';
@@ -1,100 +1 @@
1
- import { calculate24hTimeline } from './timeline.js';
2
- import { previewSchedule } from './scheduler.js';
3
- import { sendJson, readBody, rejectCrossOrigin } from './http-utils.js';
4
-
5
- const NOT_ALLOWED = { ok: false, error: 'Method not allowed' };
6
-
7
- export function handleHeartbeatPing({ store, req, res, taskId, apiToken: explicitToken, ctx }) {
8
- if (req.method !== 'POST') {
9
- sendJson(res, 405, NOT_ALLOWED);
10
- return;
11
- }
12
- const apiToken = explicitToken || store?.getSettings?.()?.apiToken;
13
- if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
14
-
15
- const task = store.get(taskId);
16
- if (!task) {
17
- sendJson(res, 404, { ok: false, error: 'Task not found' });
18
- return;
19
- }
20
- const result = typeof store.recordHeartbeat === 'function' ? store.recordHeartbeat(taskId) : null;
21
- if (!result) {
22
- sendJson(res, 500, { ok: false, error: 'Could not record heartbeat' });
23
- return;
24
- }
25
- sendJson(res, 200, result);
26
- }
27
-
28
- export async function handleSchedulePreview({ req, res, url, store, apiToken: explicitToken, ctx }) {
29
- const apiToken = explicitToken || store?.getSettings?.()?.apiToken;
30
- if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
31
-
32
- let schedule = '';
33
- let timezone = '';
34
- let count = 5;
35
-
36
- if (req.method === 'POST') {
37
- const { body, error } = await readBody(req, res);
38
- if (error) return;
39
- schedule = body.schedule || '';
40
- timezone = body.timezone || '';
41
- count = body.count || 5;
42
- } else if (req.method === 'GET') {
43
- schedule = url.searchParams.get('schedule') || '';
44
- timezone = url.searchParams.get('timezone') || '';
45
- count = parseInt(url.searchParams.get('count') || '5', 10);
46
- } else {
47
- sendJson(res, 405, NOT_ALLOWED);
48
- return;
49
- }
50
-
51
- if (!schedule) {
52
- sendJson(res, 400, { ok: false, error: 'Field "schedule" is required' });
53
- return;
54
- }
55
-
56
- try {
57
- const preview = previewSchedule(schedule, { timezone, count });
58
- sendJson(res, 200, { ok: true, ...preview });
59
- } catch (err) {
60
- sendJson(res, 400, { ok: false, error: err.message });
61
- }
62
- }
63
-
64
- export async function handleDryRunTask({ store, scheduler, req, res, id, apiToken: explicitToken, ctx }) {
65
- const apiToken = explicitToken || store?.getSettings?.()?.apiToken;
66
- if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
67
- const task = store.get(id);
68
- if (!task) {
69
- sendJson(res, 404, { ok: false, error: 'Task not found' });
70
- return;
71
- }
72
-
73
- try {
74
- const outcome = await scheduler.runTask(id, { dryRun: true });
75
- sendJson(res, 200, outcome || { ok: true, status: 'completed' });
76
- } catch (err) {
77
- sendJson(res, 500, { ok: false, error: err.message });
78
- }
79
- }
80
-
81
- export async function handleScheduleTimeline({ req, res, url, store, scheduler, apiToken: explicitToken, ctx }) {
82
- const apiToken = explicitToken || store?.getSettings?.()?.apiToken;
83
- if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
84
-
85
- if (req.method !== 'GET') {
86
- sendJson(res, 405, NOT_ALLOWED);
87
- return;
88
- }
89
-
90
- const hours = parseInt(url.searchParams.get('hours') || '24', 10);
91
- const tasks = typeof store?.list === 'function' ? store.list() : [];
92
- const maxConcurrent = store?.getSettings?.()?.maxConcurrent || 3;
93
-
94
- try {
95
- const timeline = calculate24hTimeline({ tasks, maxConcurrent, hoursCount: hours });
96
- sendJson(res, 200, { ok: true, timeline });
97
- } catch (err) {
98
- sendJson(res, 500, { ok: false, error: err.message });
99
- }
100
- }
1
+ export * from './routes/api-helpers.js';