@flowdot.ai/daemon 1.0.0
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/LICENSE +45 -0
- package/README.md +51 -0
- package/dist/goals/DependencyResolver.d.ts +54 -0
- package/dist/goals/DependencyResolver.js +329 -0
- package/dist/goals/ErrorRecovery.d.ts +133 -0
- package/dist/goals/ErrorRecovery.js +489 -0
- package/dist/goals/GoalApiClient.d.ts +81 -0
- package/dist/goals/GoalApiClient.js +743 -0
- package/dist/goals/GoalCache.d.ts +65 -0
- package/dist/goals/GoalCache.js +243 -0
- package/dist/goals/GoalCommsHandler.d.ts +150 -0
- package/dist/goals/GoalCommsHandler.js +378 -0
- package/dist/goals/GoalExporter.d.ts +164 -0
- package/dist/goals/GoalExporter.js +318 -0
- package/dist/goals/GoalImporter.d.ts +107 -0
- package/dist/goals/GoalImporter.js +345 -0
- package/dist/goals/GoalManager.d.ts +110 -0
- package/dist/goals/GoalManager.js +535 -0
- package/dist/goals/GoalReporter.d.ts +105 -0
- package/dist/goals/GoalReporter.js +534 -0
- package/dist/goals/GoalScheduler.d.ts +102 -0
- package/dist/goals/GoalScheduler.js +209 -0
- package/dist/goals/GoalValidator.d.ts +72 -0
- package/dist/goals/GoalValidator.js +657 -0
- package/dist/goals/MetaGoalEnforcer.d.ts +111 -0
- package/dist/goals/MetaGoalEnforcer.js +536 -0
- package/dist/goals/MilestoneBreaker.d.ts +74 -0
- package/dist/goals/MilestoneBreaker.js +348 -0
- package/dist/goals/PermissionBridge.d.ts +109 -0
- package/dist/goals/PermissionBridge.js +326 -0
- package/dist/goals/ProgressTracker.d.ts +113 -0
- package/dist/goals/ProgressTracker.js +324 -0
- package/dist/goals/ReviewScheduler.d.ts +106 -0
- package/dist/goals/ReviewScheduler.js +360 -0
- package/dist/goals/TaskExecutor.d.ts +116 -0
- package/dist/goals/TaskExecutor.js +370 -0
- package/dist/goals/TaskFeedback.d.ts +126 -0
- package/dist/goals/TaskFeedback.js +402 -0
- package/dist/goals/TaskGenerator.d.ts +75 -0
- package/dist/goals/TaskGenerator.js +329 -0
- package/dist/goals/TaskQueue.d.ts +84 -0
- package/dist/goals/TaskQueue.js +331 -0
- package/dist/goals/TaskSanitizer.d.ts +61 -0
- package/dist/goals/TaskSanitizer.js +464 -0
- package/dist/goals/errors.d.ts +116 -0
- package/dist/goals/errors.js +299 -0
- package/dist/goals/index.d.ts +24 -0
- package/dist/goals/index.js +23 -0
- package/dist/goals/types.d.ts +395 -0
- package/dist/goals/types.js +230 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/loop/DaemonIPC.d.ts +67 -0
- package/dist/loop/DaemonIPC.js +358 -0
- package/dist/loop/IntervalParser.d.ts +39 -0
- package/dist/loop/IntervalParser.js +217 -0
- package/dist/loop/LoopDaemon.d.ts +123 -0
- package/dist/loop/LoopDaemon.js +1821 -0
- package/dist/loop/LoopExecutor.d.ts +93 -0
- package/dist/loop/LoopExecutor.js +326 -0
- package/dist/loop/LoopManager.d.ts +79 -0
- package/dist/loop/LoopManager.js +476 -0
- package/dist/loop/LoopScheduler.d.ts +69 -0
- package/dist/loop/LoopScheduler.js +329 -0
- package/dist/loop/LoopStore.d.ts +57 -0
- package/dist/loop/LoopStore.js +406 -0
- package/dist/loop/LoopValidator.d.ts +55 -0
- package/dist/loop/LoopValidator.js +603 -0
- package/dist/loop/errors.d.ts +115 -0
- package/dist/loop/errors.js +312 -0
- package/dist/loop/index.d.ts +11 -0
- package/dist/loop/index.js +10 -0
- package/dist/loop/notifications/Notifier.d.ts +28 -0
- package/dist/loop/notifications/Notifier.js +78 -0
- package/dist/loop/notifications/SlackNotifier.d.ts +28 -0
- package/dist/loop/notifications/SlackNotifier.js +203 -0
- package/dist/loop/notifications/TerminalNotifier.d.ts +18 -0
- package/dist/loop/notifications/TerminalNotifier.js +72 -0
- package/dist/loop/notifications/WebhookNotifier.d.ts +24 -0
- package/dist/loop/notifications/WebhookNotifier.js +123 -0
- package/dist/loop/notifications/index.d.ts +24 -0
- package/dist/loop/notifications/index.js +109 -0
- package/dist/loop/types.d.ts +280 -0
- package/dist/loop/types.js +222 -0
- package/package.json +92 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
import msPkg from 'ms';
|
|
2
|
+
import { isNotifyMethod, isOnErrorAction, } from './types.js';
|
|
3
|
+
import { ValidationError, InvalidPromptError, InvalidNameError, InvalidOptionsError, InvalidIntervalError, ConfigError, } from './errors.js';
|
|
4
|
+
import { IntervalParser } from './IntervalParser.js';
|
|
5
|
+
function parseDurationString(value) {
|
|
6
|
+
return msPkg(value);
|
|
7
|
+
}
|
|
8
|
+
export const DEFAULT_VALIDATION_CONSTRAINTS = {
|
|
9
|
+
maxPromptLength: 10000,
|
|
10
|
+
minPromptLength: 1,
|
|
11
|
+
maxNameLength: 64,
|
|
12
|
+
minNameLength: 1,
|
|
13
|
+
namePattern: /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/,
|
|
14
|
+
maxWebhookUrlLength: 2048,
|
|
15
|
+
maxMaxRuns: 1000000,
|
|
16
|
+
validModelTiers: ['simple', 'capable', 'complex'],
|
|
17
|
+
};
|
|
18
|
+
export class LoopValidator {
|
|
19
|
+
constraints;
|
|
20
|
+
intervalParser;
|
|
21
|
+
constructor(constraints = {}, intervalParserOptions = {}) {
|
|
22
|
+
this.constraints = { ...DEFAULT_VALIDATION_CONSTRAINTS, ...constraints };
|
|
23
|
+
this.intervalParser = new IntervalParser(intervalParserOptions);
|
|
24
|
+
}
|
|
25
|
+
validateCreateInput(input) {
|
|
26
|
+
const errors = this.collectCreateInputErrors(input);
|
|
27
|
+
if (errors.length > 0) {
|
|
28
|
+
const first = errors[0];
|
|
29
|
+
this.throwValidationError(first);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
safeValidateCreateInput(input) {
|
|
33
|
+
const errors = this.collectCreateInputErrors(input);
|
|
34
|
+
return {
|
|
35
|
+
valid: errors.length === 0,
|
|
36
|
+
errors,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
collectCreateInputErrors(input) {
|
|
40
|
+
const errors = [];
|
|
41
|
+
const promptError = this.validatePromptField(input.prompt);
|
|
42
|
+
if (promptError)
|
|
43
|
+
errors.push(promptError);
|
|
44
|
+
const intervalError = this.validateIntervalField(input.interval);
|
|
45
|
+
if (intervalError)
|
|
46
|
+
errors.push(intervalError);
|
|
47
|
+
if (input.name !== undefined) {
|
|
48
|
+
const nameError = this.validateNameField(input.name);
|
|
49
|
+
if (nameError)
|
|
50
|
+
errors.push(nameError);
|
|
51
|
+
}
|
|
52
|
+
if (input.maxRuns !== undefined) {
|
|
53
|
+
const maxRunsError = this.validateMaxRunsField(input.maxRuns);
|
|
54
|
+
if (maxRunsError)
|
|
55
|
+
errors.push(maxRunsError);
|
|
56
|
+
}
|
|
57
|
+
if (input.expires !== undefined) {
|
|
58
|
+
const expiresError = this.validateExpiresField(input.expires);
|
|
59
|
+
if (expiresError)
|
|
60
|
+
errors.push(expiresError);
|
|
61
|
+
}
|
|
62
|
+
if (input.model !== undefined) {
|
|
63
|
+
const modelError = this.validateModelField(input.model);
|
|
64
|
+
if (modelError)
|
|
65
|
+
errors.push(modelError);
|
|
66
|
+
}
|
|
67
|
+
if (input.onError !== undefined) {
|
|
68
|
+
const onErrorError = this.validateOnErrorField(input.onError);
|
|
69
|
+
if (onErrorError)
|
|
70
|
+
errors.push(onErrorError);
|
|
71
|
+
}
|
|
72
|
+
if (input.notify !== undefined) {
|
|
73
|
+
const notifyError = this.validateNotifyField(input.notify);
|
|
74
|
+
if (notifyError)
|
|
75
|
+
errors.push(notifyError);
|
|
76
|
+
}
|
|
77
|
+
if (input.webhookUrl !== undefined) {
|
|
78
|
+
const webhookError = this.validateWebhookUrlField(input.webhookUrl);
|
|
79
|
+
if (webhookError)
|
|
80
|
+
errors.push(webhookError);
|
|
81
|
+
}
|
|
82
|
+
if (input.notify === 'webhook' && !input.webhookUrl) {
|
|
83
|
+
errors.push({
|
|
84
|
+
field: 'webhookUrl',
|
|
85
|
+
message: 'Webhook URL is required when notify is set to "webhook"',
|
|
86
|
+
value: undefined,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return errors;
|
|
90
|
+
}
|
|
91
|
+
validateUpdateInput(input) {
|
|
92
|
+
const errors = this.collectUpdateInputErrors(input);
|
|
93
|
+
if (errors.length > 0) {
|
|
94
|
+
const first = errors[0];
|
|
95
|
+
this.throwValidationError(first);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
safeValidateUpdateInput(input) {
|
|
99
|
+
const errors = this.collectUpdateInputErrors(input);
|
|
100
|
+
return {
|
|
101
|
+
valid: errors.length === 0,
|
|
102
|
+
errors,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
collectUpdateInputErrors(input) {
|
|
106
|
+
const errors = [];
|
|
107
|
+
const identifierError = this.validateIdentifierField(input.identifier);
|
|
108
|
+
if (identifierError)
|
|
109
|
+
errors.push(identifierError);
|
|
110
|
+
if (input.name !== undefined) {
|
|
111
|
+
const nameError = this.validateNameField(input.name);
|
|
112
|
+
if (nameError)
|
|
113
|
+
errors.push(nameError);
|
|
114
|
+
}
|
|
115
|
+
if (input.interval !== undefined) {
|
|
116
|
+
const intervalError = this.validateIntervalField(input.interval);
|
|
117
|
+
if (intervalError)
|
|
118
|
+
errors.push(intervalError);
|
|
119
|
+
}
|
|
120
|
+
if (input.model !== undefined) {
|
|
121
|
+
const modelError = this.validateModelField(input.model);
|
|
122
|
+
if (modelError)
|
|
123
|
+
errors.push(modelError);
|
|
124
|
+
}
|
|
125
|
+
if (input.onError !== undefined) {
|
|
126
|
+
const onErrorError = this.validateOnErrorField(input.onError);
|
|
127
|
+
if (onErrorError)
|
|
128
|
+
errors.push(onErrorError);
|
|
129
|
+
}
|
|
130
|
+
if (input.notify !== undefined) {
|
|
131
|
+
const notifyError = this.validateNotifyField(input.notify);
|
|
132
|
+
if (notifyError)
|
|
133
|
+
errors.push(notifyError);
|
|
134
|
+
}
|
|
135
|
+
return errors;
|
|
136
|
+
}
|
|
137
|
+
validatePromptField(prompt) {
|
|
138
|
+
if (typeof prompt !== 'string') {
|
|
139
|
+
return {
|
|
140
|
+
field: 'prompt',
|
|
141
|
+
message: 'Prompt must be a string',
|
|
142
|
+
value: prompt,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const trimmed = prompt.trim();
|
|
146
|
+
if (trimmed.length < this.constraints.minPromptLength) {
|
|
147
|
+
return {
|
|
148
|
+
field: 'prompt',
|
|
149
|
+
message: 'Prompt cannot be empty',
|
|
150
|
+
value: prompt,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (trimmed.length > this.constraints.maxPromptLength) {
|
|
154
|
+
return {
|
|
155
|
+
field: 'prompt',
|
|
156
|
+
message: `Prompt exceeds maximum length of ${this.constraints.maxPromptLength} characters`,
|
|
157
|
+
value: `(${trimmed.length} characters)`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
validateIntervalField(interval) {
|
|
163
|
+
if (typeof interval !== 'string') {
|
|
164
|
+
return {
|
|
165
|
+
field: 'interval',
|
|
166
|
+
message: 'Interval must be a string',
|
|
167
|
+
value: interval,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const trimmed = interval.trim();
|
|
171
|
+
if (!trimmed) {
|
|
172
|
+
return {
|
|
173
|
+
field: 'interval',
|
|
174
|
+
message: 'Interval cannot be empty',
|
|
175
|
+
value: interval,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const result = this.intervalParser.validate(trimmed);
|
|
179
|
+
if (!result.valid) {
|
|
180
|
+
return {
|
|
181
|
+
field: 'interval',
|
|
182
|
+
message: result.error ?? 'Invalid interval format',
|
|
183
|
+
value: interval,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
validateNameField(name) {
|
|
189
|
+
if (typeof name !== 'string') {
|
|
190
|
+
return {
|
|
191
|
+
field: 'name',
|
|
192
|
+
message: 'Name must be a string',
|
|
193
|
+
value: name,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const trimmed = name.trim();
|
|
197
|
+
if (trimmed.length < this.constraints.minNameLength) {
|
|
198
|
+
return {
|
|
199
|
+
field: 'name',
|
|
200
|
+
message: `Name must be at least ${this.constraints.minNameLength} character(s)`,
|
|
201
|
+
value: name,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (trimmed.length > this.constraints.maxNameLength) {
|
|
205
|
+
return {
|
|
206
|
+
field: 'name',
|
|
207
|
+
message: `Name exceeds maximum length of ${this.constraints.maxNameLength} characters`,
|
|
208
|
+
value: name,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (!this.constraints.namePattern.test(trimmed)) {
|
|
212
|
+
return {
|
|
213
|
+
field: 'name',
|
|
214
|
+
message: 'Name must start with a letter or number and contain only letters, numbers, dashes, and underscores',
|
|
215
|
+
value: name,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
validateIdentifierField(identifier) {
|
|
221
|
+
if (typeof identifier !== 'string') {
|
|
222
|
+
return {
|
|
223
|
+
field: 'identifier',
|
|
224
|
+
message: 'Identifier must be a string',
|
|
225
|
+
value: identifier,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const trimmed = identifier.trim();
|
|
229
|
+
if (!trimmed) {
|
|
230
|
+
return {
|
|
231
|
+
field: 'identifier',
|
|
232
|
+
message: 'Identifier cannot be empty',
|
|
233
|
+
value: identifier,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
validateMaxRunsField(maxRuns) {
|
|
239
|
+
if (typeof maxRuns !== 'number') {
|
|
240
|
+
return {
|
|
241
|
+
field: 'maxRuns',
|
|
242
|
+
message: 'maxRuns must be a number',
|
|
243
|
+
value: maxRuns,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (!Number.isInteger(maxRuns)) {
|
|
247
|
+
return {
|
|
248
|
+
field: 'maxRuns',
|
|
249
|
+
message: 'maxRuns must be an integer',
|
|
250
|
+
value: maxRuns,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (maxRuns < 1) {
|
|
254
|
+
return {
|
|
255
|
+
field: 'maxRuns',
|
|
256
|
+
message: 'maxRuns must be at least 1',
|
|
257
|
+
value: maxRuns,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
if (maxRuns > this.constraints.maxMaxRuns) {
|
|
261
|
+
return {
|
|
262
|
+
field: 'maxRuns',
|
|
263
|
+
message: `maxRuns cannot exceed ${this.constraints.maxMaxRuns}`,
|
|
264
|
+
value: maxRuns,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
validateExpiresField(expires) {
|
|
270
|
+
if (typeof expires !== 'string') {
|
|
271
|
+
return {
|
|
272
|
+
field: 'expires',
|
|
273
|
+
message: 'expires must be a string',
|
|
274
|
+
value: expires,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const trimmed = expires.trim();
|
|
278
|
+
if (!trimmed) {
|
|
279
|
+
return {
|
|
280
|
+
field: 'expires',
|
|
281
|
+
message: 'expires cannot be empty',
|
|
282
|
+
value: expires,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const milliseconds = parseDurationString(trimmed);
|
|
286
|
+
if (milliseconds === undefined || typeof milliseconds !== 'number' || isNaN(milliseconds)) {
|
|
287
|
+
return {
|
|
288
|
+
field: 'expires',
|
|
289
|
+
message: 'Invalid expiration duration. Use formats like "3d", "1w", "24h"',
|
|
290
|
+
value: expires,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
if (milliseconds <= 0) {
|
|
294
|
+
return {
|
|
295
|
+
field: 'expires',
|
|
296
|
+
message: 'Expiration duration must be positive',
|
|
297
|
+
value: expires,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
validateModelField(model) {
|
|
303
|
+
if (typeof model !== 'string') {
|
|
304
|
+
return {
|
|
305
|
+
field: 'model',
|
|
306
|
+
message: 'model must be a string',
|
|
307
|
+
value: model,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
if (!this.constraints.validModelTiers.includes(model)) {
|
|
311
|
+
return {
|
|
312
|
+
field: 'model',
|
|
313
|
+
message: `Invalid model tier. Must be one of: ${this.constraints.validModelTiers.join(', ')}`,
|
|
314
|
+
value: model,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
validateOnErrorField(onError) {
|
|
320
|
+
if (typeof onError !== 'string') {
|
|
321
|
+
return {
|
|
322
|
+
field: 'onError',
|
|
323
|
+
message: 'onError must be a string',
|
|
324
|
+
value: onError,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (!isOnErrorAction(onError)) {
|
|
328
|
+
return {
|
|
329
|
+
field: 'onError',
|
|
330
|
+
message: 'Invalid onError action. Must be one of: continue, pause, stop',
|
|
331
|
+
value: onError,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
validateNotifyField(notify) {
|
|
337
|
+
if (typeof notify !== 'string') {
|
|
338
|
+
return {
|
|
339
|
+
field: 'notify',
|
|
340
|
+
message: 'notify must be a string',
|
|
341
|
+
value: notify,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (!isNotifyMethod(notify)) {
|
|
345
|
+
return {
|
|
346
|
+
field: 'notify',
|
|
347
|
+
message: 'Invalid notify method. Must be one of: none, terminal, webhook, slack',
|
|
348
|
+
value: notify,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
validateWebhookUrlField(webhookUrl) {
|
|
354
|
+
if (typeof webhookUrl !== 'string') {
|
|
355
|
+
return {
|
|
356
|
+
field: 'webhookUrl',
|
|
357
|
+
message: 'webhookUrl must be a string',
|
|
358
|
+
value: webhookUrl,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
const trimmed = webhookUrl.trim();
|
|
362
|
+
if (!trimmed) {
|
|
363
|
+
return {
|
|
364
|
+
field: 'webhookUrl',
|
|
365
|
+
message: 'webhookUrl cannot be empty',
|
|
366
|
+
value: webhookUrl,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
if (trimmed.length > this.constraints.maxWebhookUrlLength) {
|
|
370
|
+
return {
|
|
371
|
+
field: 'webhookUrl',
|
|
372
|
+
message: `webhookUrl exceeds maximum length of ${this.constraints.maxWebhookUrlLength} characters`,
|
|
373
|
+
value: webhookUrl,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
const url = new URL(trimmed);
|
|
378
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
379
|
+
return {
|
|
380
|
+
field: 'webhookUrl',
|
|
381
|
+
message: 'webhookUrl must use http or https protocol',
|
|
382
|
+
value: webhookUrl,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
return {
|
|
388
|
+
field: 'webhookUrl',
|
|
389
|
+
message: 'webhookUrl must be a valid URL',
|
|
390
|
+
value: webhookUrl,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
validateConfig(config) {
|
|
396
|
+
const errors = this.collectConfigErrors(config);
|
|
397
|
+
if (errors.length > 0) {
|
|
398
|
+
const first = errors[0];
|
|
399
|
+
throw new ConfigError(first.field, first.message);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
safeValidateConfig(config) {
|
|
403
|
+
const errors = this.collectConfigErrors(config);
|
|
404
|
+
return {
|
|
405
|
+
valid: errors.length === 0,
|
|
406
|
+
errors,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
collectConfigErrors(config) {
|
|
410
|
+
const errors = [];
|
|
411
|
+
if (config.defaultExpiration !== undefined) {
|
|
412
|
+
const msValue = parseDurationString(config.defaultExpiration);
|
|
413
|
+
if (msValue === undefined || typeof msValue !== 'number' || msValue <= 0) {
|
|
414
|
+
errors.push({
|
|
415
|
+
field: 'defaultExpiration',
|
|
416
|
+
message: 'Must be a valid positive duration (e.g., "3d", "1w")',
|
|
417
|
+
value: config.defaultExpiration,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (config.defaultOnError !== undefined && !isOnErrorAction(config.defaultOnError)) {
|
|
422
|
+
errors.push({
|
|
423
|
+
field: 'defaultOnError',
|
|
424
|
+
message: 'Must be one of: continue, pause, stop',
|
|
425
|
+
value: config.defaultOnError,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
if (config.defaultNotify !== undefined && !isNotifyMethod(config.defaultNotify)) {
|
|
429
|
+
errors.push({
|
|
430
|
+
field: 'defaultNotify',
|
|
431
|
+
message: 'Must be one of: none, terminal, webhook, slack',
|
|
432
|
+
value: config.defaultNotify,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
if (config.maxConcurrentLoops !== undefined) {
|
|
436
|
+
if (typeof config.maxConcurrentLoops !== 'number' ||
|
|
437
|
+
!Number.isInteger(config.maxConcurrentLoops) ||
|
|
438
|
+
config.maxConcurrentLoops < 1) {
|
|
439
|
+
errors.push({
|
|
440
|
+
field: 'maxConcurrentLoops',
|
|
441
|
+
message: 'Must be a positive integer',
|
|
442
|
+
value: config.maxConcurrentLoops,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (config.minInterval !== undefined) {
|
|
447
|
+
const msValue = parseDurationString(config.minInterval);
|
|
448
|
+
if (msValue === undefined || typeof msValue !== 'number' || msValue <= 0) {
|
|
449
|
+
errors.push({
|
|
450
|
+
field: 'minInterval',
|
|
451
|
+
message: 'Must be a valid positive duration (e.g., "1m", "30s")',
|
|
452
|
+
value: config.minInterval,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (config.maxRunDuration !== undefined) {
|
|
457
|
+
const msValue = parseDurationString(config.maxRunDuration);
|
|
458
|
+
if (msValue === undefined || typeof msValue !== 'number' || msValue <= 0) {
|
|
459
|
+
errors.push({
|
|
460
|
+
field: 'maxRunDuration',
|
|
461
|
+
message: 'Must be a valid positive duration (e.g., "30m", "1h")',
|
|
462
|
+
value: config.maxRunDuration,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (config.errorThreshold !== undefined) {
|
|
467
|
+
if (typeof config.errorThreshold !== 'number' ||
|
|
468
|
+
!Number.isInteger(config.errorThreshold) ||
|
|
469
|
+
config.errorThreshold < 1) {
|
|
470
|
+
errors.push({
|
|
471
|
+
field: 'errorThreshold',
|
|
472
|
+
message: 'Must be a positive integer',
|
|
473
|
+
value: config.errorThreshold,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (config.daemon !== undefined) {
|
|
478
|
+
if (config.daemon.port !== undefined) {
|
|
479
|
+
if (typeof config.daemon.port !== 'number' ||
|
|
480
|
+
!Number.isInteger(config.daemon.port) ||
|
|
481
|
+
config.daemon.port < 1 ||
|
|
482
|
+
config.daemon.port > 65535) {
|
|
483
|
+
errors.push({
|
|
484
|
+
field: 'daemon.port',
|
|
485
|
+
message: 'Must be a valid port number (1-65535)',
|
|
486
|
+
value: config.daemon.port,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (config.daemon.logLevel !== undefined) {
|
|
491
|
+
const validLevels = ['debug', 'info', 'warn', 'error'];
|
|
492
|
+
if (!validLevels.includes(config.daemon.logLevel)) {
|
|
493
|
+
errors.push({
|
|
494
|
+
field: 'daemon.logLevel',
|
|
495
|
+
message: `Must be one of: ${validLevels.join(', ')}`,
|
|
496
|
+
value: config.daemon.logLevel,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (config.daemon.healthCheckInterval !== undefined) {
|
|
501
|
+
if (typeof config.daemon.healthCheckInterval !== 'number' ||
|
|
502
|
+
config.daemon.healthCheckInterval < 1000) {
|
|
503
|
+
errors.push({
|
|
504
|
+
field: 'daemon.healthCheckInterval',
|
|
505
|
+
message: 'Must be at least 1000 milliseconds',
|
|
506
|
+
value: config.daemon.healthCheckInterval,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (config.webhooks !== undefined) {
|
|
512
|
+
if (config.webhooks.defaultUrl !== undefined && config.webhooks.defaultUrl !== null) {
|
|
513
|
+
const urlError = this.validateWebhookUrlField(config.webhooks.defaultUrl);
|
|
514
|
+
if (urlError) {
|
|
515
|
+
errors.push({
|
|
516
|
+
field: 'webhooks.defaultUrl',
|
|
517
|
+
message: urlError.message,
|
|
518
|
+
value: config.webhooks.defaultUrl,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (config.webhooks.timeout !== undefined) {
|
|
523
|
+
if (typeof config.webhooks.timeout !== 'number' ||
|
|
524
|
+
config.webhooks.timeout < 1000) {
|
|
525
|
+
errors.push({
|
|
526
|
+
field: 'webhooks.timeout',
|
|
527
|
+
message: 'Must be at least 1000 milliseconds',
|
|
528
|
+
value: config.webhooks.timeout,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (config.webhooks.retries !== undefined) {
|
|
533
|
+
if (typeof config.webhooks.retries !== 'number' ||
|
|
534
|
+
!Number.isInteger(config.webhooks.retries) ||
|
|
535
|
+
config.webhooks.retries < 0) {
|
|
536
|
+
errors.push({
|
|
537
|
+
field: 'webhooks.retries',
|
|
538
|
+
message: 'Must be a non-negative integer',
|
|
539
|
+
value: config.webhooks.retries,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return errors;
|
|
545
|
+
}
|
|
546
|
+
validateName(name) {
|
|
547
|
+
const error = this.validateNameField(name);
|
|
548
|
+
if (error) {
|
|
549
|
+
throw new InvalidNameError(name, error.message);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
validatePrompt(prompt) {
|
|
553
|
+
const error = this.validatePromptField(prompt);
|
|
554
|
+
if (error) {
|
|
555
|
+
throw new InvalidPromptError(error.message);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
validateInterval(interval) {
|
|
559
|
+
const error = this.validateIntervalField(interval);
|
|
560
|
+
if (error) {
|
|
561
|
+
throw new InvalidIntervalError(interval, error.message);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
throwValidationError(issue) {
|
|
565
|
+
switch (issue.field) {
|
|
566
|
+
case 'prompt':
|
|
567
|
+
throw new InvalidPromptError(issue.message);
|
|
568
|
+
case 'name':
|
|
569
|
+
throw new InvalidNameError(String(issue.value ?? ''), issue.message);
|
|
570
|
+
case 'interval':
|
|
571
|
+
throw new InvalidIntervalError(String(issue.value ?? ''), issue.message);
|
|
572
|
+
case 'model':
|
|
573
|
+
case 'onError':
|
|
574
|
+
case 'notify':
|
|
575
|
+
case 'webhookUrl':
|
|
576
|
+
case 'maxRuns':
|
|
577
|
+
case 'expires':
|
|
578
|
+
case 'quiet':
|
|
579
|
+
throw new InvalidOptionsError(issue.field, issue.value, issue.message);
|
|
580
|
+
default:
|
|
581
|
+
throw new ValidationError(issue.field, issue.value, issue.message);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
export function createLoopValidator(constraints, intervalParserOptions) {
|
|
586
|
+
return new LoopValidator(constraints, intervalParserOptions);
|
|
587
|
+
}
|
|
588
|
+
export function validateCreateInput(input) {
|
|
589
|
+
const validator = new LoopValidator();
|
|
590
|
+
validator.validateCreateInput(input);
|
|
591
|
+
}
|
|
592
|
+
export function validateUpdateInput(input) {
|
|
593
|
+
const validator = new LoopValidator();
|
|
594
|
+
validator.validateUpdateInput(input);
|
|
595
|
+
}
|
|
596
|
+
export function validateLoopName(name) {
|
|
597
|
+
const validator = new LoopValidator();
|
|
598
|
+
validator.validateName(name);
|
|
599
|
+
}
|
|
600
|
+
export function validatePrompt(prompt) {
|
|
601
|
+
const validator = new LoopValidator();
|
|
602
|
+
validator.validatePrompt(prompt);
|
|
603
|
+
}
|