@minded-ai/mindedjs 3.0.4-beta.3 → 3.0.6-beta.1
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/dist/cli/index.js +6 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/validateFlow.d.ts +47 -0
- package/dist/cli/validateFlow.d.ts.map +1 -0
- package/dist/cli/validateFlow.js +454 -0
- package/dist/cli/validateFlow.js.map +1 -0
- package/dist/playbooks/playbooks.d.ts +2 -1
- package/dist/playbooks/playbooks.d.ts.map +1 -1
- package/dist/playbooks/playbooks.js +8 -2
- package/dist/playbooks/playbooks.js.map +1 -1
- package/docs/.gitbook/assets/agent-lifecycle-security.svg +4 -0
- package/docs/.gitbook/assets/credentials-lifecycle.svg +4 -0
- package/docs/.gitbook/assets/rpa-execution-flow.svg +4 -0
- package/docs/.gitbook/assets/system-overview.svg +4 -0
- package/docs/.gitbook/assets/tenant-isolation-overview.svg +4 -0
- package/docs/SUMMARY.md +1 -0
- package/docs/platform/security-architecture.md +343 -0
- package/package.json +2 -1
- package/src/cli/index.ts +5 -1
- package/src/cli/validateFlow.ts +502 -0
- package/src/playbooks/playbooks.ts +10 -3
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as yaml from 'js-yaml';
|
|
4
|
+
import { CronExpressionParser } from 'cron-parser';
|
|
5
|
+
|
|
6
|
+
interface ValidationResult {
|
|
7
|
+
success: boolean;
|
|
8
|
+
flowName?: string;
|
|
9
|
+
stats?: {
|
|
10
|
+
nodes: number;
|
|
11
|
+
edges: number;
|
|
12
|
+
nodeTypes: string[];
|
|
13
|
+
edgeTypes: string[];
|
|
14
|
+
};
|
|
15
|
+
errors?: string[];
|
|
16
|
+
warnings?: string[];
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates a cron expression format using cron-parser.
|
|
22
|
+
* Cron format: minute hour day month day-of-week (5 fields)
|
|
23
|
+
*/
|
|
24
|
+
export function validateCronExpression(cronExpression: string): { valid: boolean; error?: string } {
|
|
25
|
+
if (typeof cronExpression !== 'string' || cronExpression.trim() === '') {
|
|
26
|
+
return { valid: false, error: 'Cron expression must be a non-empty string' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
CronExpressionParser.parse(cronExpression.trim());
|
|
31
|
+
return { valid: true };
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return {
|
|
34
|
+
valid: false,
|
|
35
|
+
error: error instanceof Error ? error.message : 'Invalid cron expression format',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Validates an IANA timezone identifier.
|
|
42
|
+
* Uses Intl.DateTimeFormat to check if the timezone is valid.
|
|
43
|
+
*/
|
|
44
|
+
export function validateTimezone(timezone: string): { valid: boolean; error?: string } {
|
|
45
|
+
if (typeof timezone !== 'string' || timezone.trim() === '') {
|
|
46
|
+
return { valid: false, error: 'Timezone must be a non-empty string' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Try to create a date formatter with the timezone
|
|
51
|
+
const formatter = new Intl.DateTimeFormat('en-US', { timeZone: timezone });
|
|
52
|
+
// Test if the timezone is valid by trying to format a date
|
|
53
|
+
formatter.format(new Date());
|
|
54
|
+
return { valid: true };
|
|
55
|
+
} catch {
|
|
56
|
+
return {
|
|
57
|
+
valid: false,
|
|
58
|
+
error: `Invalid IANA timezone identifier: "${timezone}"`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validates a schedule trigger node configuration.
|
|
65
|
+
* Returns an array of error messages (empty if valid).
|
|
66
|
+
*/
|
|
67
|
+
export function validateScheduleTrigger(
|
|
68
|
+
node: any,
|
|
69
|
+
nodeIndex: string
|
|
70
|
+
): { errors: string[]; warnings: string[] } {
|
|
71
|
+
const errors: string[] = [];
|
|
72
|
+
const warnings: string[] = [];
|
|
73
|
+
|
|
74
|
+
if (!node.cronExpression) {
|
|
75
|
+
errors.push(`${nodeIndex} "${node.name}": schedule trigger missing required field "cronExpression"`);
|
|
76
|
+
} else if (typeof node.cronExpression !== 'string') {
|
|
77
|
+
errors.push(`${nodeIndex} "${node.name}": schedule trigger "cronExpression" must be a string`);
|
|
78
|
+
} else {
|
|
79
|
+
const cronValidation = validateCronExpression(node.cronExpression);
|
|
80
|
+
if (!cronValidation.valid) {
|
|
81
|
+
errors.push(`${nodeIndex} "${node.name}": invalid cron expression - ${cronValidation.error}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (node.timezone !== undefined) {
|
|
86
|
+
if (typeof node.timezone !== 'string') {
|
|
87
|
+
errors.push(`${nodeIndex} "${node.name}": schedule trigger "timezone" must be a string`);
|
|
88
|
+
} else {
|
|
89
|
+
const timezoneValidation = validateTimezone(node.timezone);
|
|
90
|
+
if (!timezoneValidation.valid) {
|
|
91
|
+
errors.push(`${nodeIndex} "${node.name}": ${timezoneValidation.error}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { errors, warnings };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validates a MindedJS YAML flow file.
|
|
101
|
+
*/
|
|
102
|
+
export function validateFlow(flowPath: string): ValidationResult {
|
|
103
|
+
// Resolve the path
|
|
104
|
+
const resolvedPath = path.isAbsolute(flowPath) ? flowPath : path.join(process.cwd(), flowPath);
|
|
105
|
+
|
|
106
|
+
// Read the file
|
|
107
|
+
let flowContent: string;
|
|
108
|
+
try {
|
|
109
|
+
flowContent = fs.readFileSync(resolvedPath, 'utf-8');
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return {
|
|
112
|
+
success: false,
|
|
113
|
+
message: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Parse YAML
|
|
118
|
+
let flow: any;
|
|
119
|
+
try {
|
|
120
|
+
flow = yaml.load(flowContent);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return {
|
|
123
|
+
success: false,
|
|
124
|
+
message: `Invalid YAML syntax: ${error instanceof Error ? error.message : String(error)}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Validate flow structure
|
|
129
|
+
const errors: string[] = [];
|
|
130
|
+
const warnings: string[] = [];
|
|
131
|
+
|
|
132
|
+
// Check for required top-level fields
|
|
133
|
+
if (!flow.name) {
|
|
134
|
+
errors.push('Flow is missing required field: "name"');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!flow.nodes || !Array.isArray(flow.nodes)) {
|
|
138
|
+
errors.push('Flow is missing required field: "nodes" (must be an array)');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!flow.edges || !Array.isArray(flow.edges)) {
|
|
142
|
+
errors.push('Flow is missing required field: "edges" (must be an array)');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// If basic structure is invalid, return early
|
|
146
|
+
if (errors.length > 0) {
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
errors,
|
|
150
|
+
warnings,
|
|
151
|
+
message: `Flow validation failed with ${errors.length} error(s)`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Create a set of node names for validation
|
|
156
|
+
const nodeNames = new Set<string>();
|
|
157
|
+
const nodesByName = new Map<string, any>();
|
|
158
|
+
|
|
159
|
+
// Validate nodes
|
|
160
|
+
for (let i = 0; i < flow.nodes.length; i++) {
|
|
161
|
+
const node = flow.nodes[i];
|
|
162
|
+
const nodeIndex = `Node ${i + 1}`;
|
|
163
|
+
|
|
164
|
+
// Check for required name field
|
|
165
|
+
if (!node.name) {
|
|
166
|
+
errors.push(`${nodeIndex}: Missing required field "name"`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check for duplicate node names
|
|
171
|
+
if (nodeNames.has(node.name)) {
|
|
172
|
+
errors.push(`${nodeIndex} "${node.name}": Duplicate node name`);
|
|
173
|
+
}
|
|
174
|
+
nodeNames.add(node.name);
|
|
175
|
+
nodesByName.set(node.name, node);
|
|
176
|
+
|
|
177
|
+
// Check for required type field
|
|
178
|
+
if (!node.type) {
|
|
179
|
+
errors.push(`${nodeIndex} "${node.name}": Missing required field "type"`);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Validate node based on type
|
|
184
|
+
switch (node.type) {
|
|
185
|
+
case 'trigger':
|
|
186
|
+
if (!node.triggerType) {
|
|
187
|
+
errors.push(`${nodeIndex} "${node.name}": trigger node missing required field "triggerType"`);
|
|
188
|
+
} else if (!['manual', 'app', 'webhook', 'schedule'].includes(node.triggerType)) {
|
|
189
|
+
errors.push(
|
|
190
|
+
`${nodeIndex} "${node.name}": invalid triggerType "${node.triggerType}". Must be one of: manual, app, webhook, schedule`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
if (node.triggerType === 'app' && !node.appTriggerId) {
|
|
194
|
+
errors.push(`${nodeIndex} "${node.name}": app trigger missing required field "appTriggerId"`);
|
|
195
|
+
}
|
|
196
|
+
if (node.triggerType === 'schedule') {
|
|
197
|
+
const scheduleValidation = validateScheduleTrigger(node, nodeIndex);
|
|
198
|
+
errors.push(...scheduleValidation.errors);
|
|
199
|
+
warnings.push(...scheduleValidation.warnings);
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
|
|
203
|
+
case 'promptNode':
|
|
204
|
+
if (!node.prompt) {
|
|
205
|
+
errors.push(`${nodeIndex} "${node.name}": promptNode missing required field "prompt"`);
|
|
206
|
+
}
|
|
207
|
+
if (!node.displayName || node.displayName.trim() === '') {
|
|
208
|
+
errors.push(`${nodeIndex} "${node.name}": promptNode missing required non-empty field "displayName"`);
|
|
209
|
+
}
|
|
210
|
+
if (node.llmConfig) {
|
|
211
|
+
if (!node.llmConfig.name) {
|
|
212
|
+
errors.push(`${nodeIndex} "${node.name}": llmConfig missing required field "name"`);
|
|
213
|
+
}
|
|
214
|
+
if (!node.llmConfig.properties) {
|
|
215
|
+
warnings.push(`${nodeIndex} "${node.name}": llmConfig missing "properties" field`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
|
|
220
|
+
case 'thinking':
|
|
221
|
+
if (!node.prompt) {
|
|
222
|
+
errors.push(`${nodeIndex} "${node.name}": thinking node missing required field "prompt"`);
|
|
223
|
+
}
|
|
224
|
+
if (node.humanInTheLoop !== undefined) {
|
|
225
|
+
errors.push(
|
|
226
|
+
`${nodeIndex} "${node.name}": thinking node does not support "humanInTheLoop" property (only prompt nodes support this)`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
if (node.canStayOnNode !== undefined) {
|
|
230
|
+
errors.push(
|
|
231
|
+
`${nodeIndex} "${node.name}": thinking node does not support "canStayOnNode" property (only prompt nodes support this)`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
if (node.llmConfig) {
|
|
235
|
+
if (!node.llmConfig.name) {
|
|
236
|
+
errors.push(`${nodeIndex} "${node.name}": llmConfig missing required field "name"`);
|
|
237
|
+
}
|
|
238
|
+
if (!node.llmConfig.properties) {
|
|
239
|
+
warnings.push(`${nodeIndex} "${node.name}": llmConfig missing "properties" field`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
break;
|
|
243
|
+
|
|
244
|
+
case 'tool':
|
|
245
|
+
if (!node.toolName) {
|
|
246
|
+
errors.push(`${nodeIndex} "${node.name}": tool node missing required field "toolName"`);
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
|
|
250
|
+
case 'junction':
|
|
251
|
+
// Junction only requires name and type, which we already checked
|
|
252
|
+
break;
|
|
253
|
+
|
|
254
|
+
case 'jumpToNode':
|
|
255
|
+
if (!node.targetNodeId) {
|
|
256
|
+
errors.push(`${nodeIndex} "${node.name}": jumpToNode missing required field "targetNodeId"`);
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
|
|
260
|
+
case 'browserTask':
|
|
261
|
+
if (!node.prompt) {
|
|
262
|
+
errors.push(`${nodeIndex} "${node.name}": browserTask node missing required field "prompt"`);
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
|
|
266
|
+
default:
|
|
267
|
+
warnings.push(`${nodeIndex} "${node.name}": unknown node type "${node.type}"`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Create a map to track which nodes have edges
|
|
272
|
+
const nodesWithEdges = new Set<string>();
|
|
273
|
+
|
|
274
|
+
// Validate edges
|
|
275
|
+
for (let i = 0; i < flow.edges.length; i++) {
|
|
276
|
+
const edge = flow.edges[i];
|
|
277
|
+
const edgeIndex = `Edge ${i + 1}`;
|
|
278
|
+
|
|
279
|
+
// Check for required fields
|
|
280
|
+
if (!edge.source) {
|
|
281
|
+
errors.push(`${edgeIndex}: Missing required field "source"`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!edge.target) {
|
|
286
|
+
errors.push(`${edgeIndex}: Missing required field "target"`);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!edge.type) {
|
|
291
|
+
errors.push(`${edgeIndex}: Missing required field "type"`);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Track nodes that have edges
|
|
296
|
+
nodesWithEdges.add(edge.source);
|
|
297
|
+
nodesWithEdges.add(edge.target);
|
|
298
|
+
|
|
299
|
+
// Check if source and target nodes exist
|
|
300
|
+
if (!nodeNames.has(edge.source)) {
|
|
301
|
+
errors.push(`${edgeIndex}: source node "${edge.source}" does not exist`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (!nodeNames.has(edge.target)) {
|
|
305
|
+
errors.push(`${edgeIndex}: target node "${edge.target}" does not exist`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Validate edge based on type
|
|
309
|
+
switch (edge.type) {
|
|
310
|
+
case 'stepForward':
|
|
311
|
+
// stepForward only requires source, target, and type
|
|
312
|
+
break;
|
|
313
|
+
|
|
314
|
+
case 'promptCondition':
|
|
315
|
+
if (!edge.prompt) {
|
|
316
|
+
errors.push(`${edgeIndex}: promptCondition edge missing required field "prompt"`);
|
|
317
|
+
}
|
|
318
|
+
break;
|
|
319
|
+
|
|
320
|
+
case 'logicalCondition':
|
|
321
|
+
if (!edge.condition) {
|
|
322
|
+
errors.push(`${edgeIndex}: logicalCondition edge missing required field "condition"`);
|
|
323
|
+
}
|
|
324
|
+
break;
|
|
325
|
+
|
|
326
|
+
default:
|
|
327
|
+
warnings.push(`${edgeIndex}: unknown edge type "${edge.type}"`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Check for orphaned nodes (nodes without any edges)
|
|
332
|
+
// Note: Trigger nodes can be entry points without incoming edges
|
|
333
|
+
for (const nodeName of nodeNames) {
|
|
334
|
+
const node = nodesByName.get(nodeName);
|
|
335
|
+
if (!nodesWithEdges.has(nodeName) && node.type !== 'trigger') {
|
|
336
|
+
warnings.push(`Node "${nodeName}": orphaned node (not connected to any edges)`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Check for trigger nodes - at least one trigger should exist
|
|
341
|
+
const triggerNodes = flow.nodes.filter((n: any) => n.type === 'trigger');
|
|
342
|
+
if (triggerNodes.length === 0) {
|
|
343
|
+
warnings.push('Flow has no trigger nodes (entry points)');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const success = errors.length === 0;
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
success,
|
|
350
|
+
flowName: flow.name,
|
|
351
|
+
stats: {
|
|
352
|
+
nodes: flow.nodes.length,
|
|
353
|
+
edges: flow.edges.length,
|
|
354
|
+
nodeTypes: [...new Set(flow.nodes.map((n: any) => n.type))] as string[],
|
|
355
|
+
edgeTypes: [...new Set(flow.edges.map((e: any) => e.type))] as string[],
|
|
356
|
+
},
|
|
357
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
358
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
359
|
+
message:
|
|
360
|
+
errors.length === 0
|
|
361
|
+
? warnings.length === 0
|
|
362
|
+
? 'Flow is valid with no issues'
|
|
363
|
+
: `Flow is valid but has ${warnings.length} warning(s)`
|
|
364
|
+
: `Flow validation failed with ${errors.length} error(s)`,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
interface MindedConfig {
|
|
369
|
+
flows?: string[];
|
|
370
|
+
tools?: string[];
|
|
371
|
+
playbooks?: string[];
|
|
372
|
+
agent?: string;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Finds all YAML files in a directory recursively.
|
|
377
|
+
*/
|
|
378
|
+
function findYamlFiles(dir: string): string[] {
|
|
379
|
+
const files: string[] = [];
|
|
380
|
+
|
|
381
|
+
if (!fs.existsSync(dir)) {
|
|
382
|
+
return files;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
386
|
+
|
|
387
|
+
for (const entry of entries) {
|
|
388
|
+
const fullPath = path.join(dir, entry.name);
|
|
389
|
+
if (entry.isDirectory()) {
|
|
390
|
+
files.push(...findYamlFiles(fullPath));
|
|
391
|
+
} else if (entry.isFile() && (entry.name.endsWith('.yaml') || entry.name.endsWith('.yml'))) {
|
|
392
|
+
files.push(fullPath);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return files;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* CLI command handler for validate
|
|
401
|
+
*/
|
|
402
|
+
export function runValidateCommand(): void {
|
|
403
|
+
const mindedConfigPath = path.join(process.cwd(), 'minded.json');
|
|
404
|
+
|
|
405
|
+
// Check if minded.json exists
|
|
406
|
+
if (!fs.existsSync(mindedConfigPath)) {
|
|
407
|
+
console.error('Error: minded.json not found in the current directory');
|
|
408
|
+
console.error('Make sure you are running this command from a MindedJS agent project root');
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Read and parse minded.json
|
|
413
|
+
let mindedConfig: MindedConfig;
|
|
414
|
+
try {
|
|
415
|
+
const configContent = fs.readFileSync(mindedConfigPath, 'utf8');
|
|
416
|
+
mindedConfig = JSON.parse(configContent);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
console.error(`Error: Failed to read or parse minded.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Get flows folders
|
|
423
|
+
const flowsFolders = mindedConfig.flows;
|
|
424
|
+
if (!flowsFolders || flowsFolders.length === 0) {
|
|
425
|
+
console.error('Error: No flows folders configured in minded.json');
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Find all YAML files in flow folders
|
|
430
|
+
const flowFiles: string[] = [];
|
|
431
|
+
for (const folder of flowsFolders) {
|
|
432
|
+
const resolvedFolder = path.isAbsolute(folder) ? folder : path.join(process.cwd(), folder);
|
|
433
|
+
flowFiles.push(...findYamlFiles(resolvedFolder));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (flowFiles.length === 0) {
|
|
437
|
+
console.error('Error: No flow YAML files found in configured flows folders');
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
console.log(`\nValidating ${flowFiles.length} flow(s)...\n`);
|
|
442
|
+
|
|
443
|
+
// Validate each flow
|
|
444
|
+
const results: { path: string; result: ValidationResult }[] = [];
|
|
445
|
+
let totalErrors = 0;
|
|
446
|
+
let totalWarnings = 0;
|
|
447
|
+
|
|
448
|
+
for (const flowFile of flowFiles) {
|
|
449
|
+
const result = validateFlow(flowFile);
|
|
450
|
+
results.push({ path: flowFile, result });
|
|
451
|
+
totalErrors += result.errors?.length ?? 0;
|
|
452
|
+
totalWarnings += result.warnings?.length ?? 0;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Print results for each flow
|
|
456
|
+
for (const { path: flowPath, result } of results) {
|
|
457
|
+
const relativePath = path.relative(process.cwd(), flowPath);
|
|
458
|
+
const statusIcon = result.success ? '✅' : '❌';
|
|
459
|
+
|
|
460
|
+
console.log(`${statusIcon} ${relativePath}`);
|
|
461
|
+
|
|
462
|
+
if (result.flowName) {
|
|
463
|
+
console.log(` Flow: ${result.flowName}`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (result.stats) {
|
|
467
|
+
console.log(` Stats: ${result.stats.nodes} nodes, ${result.stats.edges} edges`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (result.errors && result.errors.length > 0) {
|
|
471
|
+
console.log(' Errors:');
|
|
472
|
+
result.errors.forEach((error) => {
|
|
473
|
+
console.log(` ❌ ${error}`);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
478
|
+
console.log(' Warnings:');
|
|
479
|
+
result.warnings.forEach((warning) => {
|
|
480
|
+
console.log(` ⚠️ ${warning}`);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
console.log('');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Print summary
|
|
488
|
+
const allPassed = totalErrors === 0;
|
|
489
|
+
console.log('─'.repeat(50));
|
|
490
|
+
|
|
491
|
+
if (allPassed) {
|
|
492
|
+
console.log(`\n✅ All ${flowFiles.length} flow(s) validated successfully!`);
|
|
493
|
+
} else {
|
|
494
|
+
console.log(`\n❌ Validation failed!`);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
console.log(` Total: ${flowFiles.length} flow(s), ${totalErrors} error(s), ${totalWarnings} warning(s)\n`);
|
|
498
|
+
|
|
499
|
+
if (!allPassed) {
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
@@ -11,7 +11,8 @@ import { Environment } from '../types/Platform.types';
|
|
|
11
11
|
export type Playbook = {
|
|
12
12
|
id: string;
|
|
13
13
|
name: string;
|
|
14
|
-
blocks
|
|
14
|
+
blocks?: OutputBlockData[];
|
|
15
|
+
prompt?: string;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -147,7 +148,7 @@ function loadPlaybooksFromDirectories(directories: string[]): Playbook[] {
|
|
|
147
148
|
const fileContent = fs.readFileSync(file, 'utf8');
|
|
148
149
|
const playbook = yaml.load(fileContent) as Playbook;
|
|
149
150
|
|
|
150
|
-
if (playbook && playbook.name && playbook.blocks) {
|
|
151
|
+
if (playbook && playbook.name && (playbook.blocks || playbook.prompt)) {
|
|
151
152
|
playbooks.push(playbook);
|
|
152
153
|
logger.info({ msg: `Loaded playbook: ${playbook.name} from ${file}` });
|
|
153
154
|
} else {
|
|
@@ -191,7 +192,13 @@ export function combinePlaybooks(playbooks: Playbook[]): string | null {
|
|
|
191
192
|
|
|
192
193
|
// Combine all playbooks into sections
|
|
193
194
|
const sections = playbooks.map((playbook) => {
|
|
194
|
-
|
|
195
|
+
let markdownContent = playbook.prompt;
|
|
196
|
+
if (!markdownContent && playbook.blocks) {
|
|
197
|
+
markdownContent = editorJsToMarkdown(playbook.blocks);
|
|
198
|
+
}
|
|
199
|
+
if (!markdownContent) {
|
|
200
|
+
throw new Error('Playbook must have either a prompt or blocks');
|
|
201
|
+
}
|
|
195
202
|
return `# ${playbook.name}\n${markdownContent}`;
|
|
196
203
|
});
|
|
197
204
|
|