@zibby/core 0.1.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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -0
  3. package/package.json +94 -0
  4. package/src/agents/base.js +361 -0
  5. package/src/constants.js +47 -0
  6. package/src/enrichment/base.js +49 -0
  7. package/src/enrichment/enrichers/accessibility-enricher.js +197 -0
  8. package/src/enrichment/enrichers/dom-enricher.js +171 -0
  9. package/src/enrichment/enrichers/page-state-enricher.js +129 -0
  10. package/src/enrichment/enrichers/position-enricher.js +67 -0
  11. package/src/enrichment/index.js +96 -0
  12. package/src/enrichment/mcp-integration.js +149 -0
  13. package/src/enrichment/mcp-ref-enricher.js +78 -0
  14. package/src/enrichment/pipeline.js +192 -0
  15. package/src/enrichment/trace-text-enricher.js +115 -0
  16. package/src/framework/AGENTS.md +98 -0
  17. package/src/framework/agents/base.js +72 -0
  18. package/src/framework/agents/claude-strategy.js +278 -0
  19. package/src/framework/agents/cursor-strategy.js +459 -0
  20. package/src/framework/agents/index.js +105 -0
  21. package/src/framework/agents/utils/cursor-output-formatter.js +67 -0
  22. package/src/framework/agents/utils/openai-proxy-formatter.js +249 -0
  23. package/src/framework/code-generator.js +301 -0
  24. package/src/framework/constants.js +33 -0
  25. package/src/framework/context-loader.js +101 -0
  26. package/src/framework/function-bridge.js +78 -0
  27. package/src/framework/function-skill-registry.js +20 -0
  28. package/src/framework/graph-compiler.js +342 -0
  29. package/src/framework/graph.js +610 -0
  30. package/src/framework/index.js +28 -0
  31. package/src/framework/node-registry.js +163 -0
  32. package/src/framework/node.js +259 -0
  33. package/src/framework/output-parser.js +71 -0
  34. package/src/framework/skill-registry.js +55 -0
  35. package/src/framework/state-utils.js +52 -0
  36. package/src/framework/state.js +67 -0
  37. package/src/framework/tool-resolver.js +65 -0
  38. package/src/index.js +342 -0
  39. package/src/runtime/generation/base.js +46 -0
  40. package/src/runtime/generation/index.js +70 -0
  41. package/src/runtime/generation/mcp-ref-strategy.js +197 -0
  42. package/src/runtime/generation/stable-id-strategy.js +170 -0
  43. package/src/runtime/stable-id-runtime.js +248 -0
  44. package/src/runtime/verification/base.js +44 -0
  45. package/src/runtime/verification/index.js +67 -0
  46. package/src/runtime/verification/playwright-json-strategy.js +119 -0
  47. package/src/runtime/zibby-runtime.js +299 -0
  48. package/src/sync/index.js +2 -0
  49. package/src/sync/uploader.js +29 -0
  50. package/src/tools/run-playwright-test.js +158 -0
  51. package/src/utils/adf-converter.js +68 -0
  52. package/src/utils/ast-utils.js +37 -0
  53. package/src/utils/ci-setup.js +124 -0
  54. package/src/utils/cursor-utils.js +71 -0
  55. package/src/utils/logger.js +144 -0
  56. package/src/utils/mcp-config-writer.js +115 -0
  57. package/src/utils/node-schema-parser.js +522 -0
  58. package/src/utils/post-process-events.js +55 -0
  59. package/src/utils/result-handler.js +102 -0
  60. package/src/utils/ripple-effect.js +84 -0
  61. package/src/utils/selector-generator.js +239 -0
  62. package/src/utils/streaming-parser.js +387 -0
  63. package/src/utils/test-post-processor.js +211 -0
  64. package/src/utils/timeline.js +217 -0
  65. package/src/utils/trace-parser.js +325 -0
  66. package/src/utils/video-organizer.js +91 -0
  67. package/templates/browser-test-automation/README.md +114 -0
  68. package/templates/browser-test-automation/graph.js +54 -0
  69. package/templates/browser-test-automation/nodes/execute-live.js +250 -0
  70. package/templates/browser-test-automation/nodes/generate-script.js +77 -0
  71. package/templates/browser-test-automation/nodes/index.js +3 -0
  72. package/templates/browser-test-automation/nodes/preflight.js +59 -0
  73. package/templates/browser-test-automation/nodes/utils.js +154 -0
  74. package/templates/browser-test-automation/result-handler.js +286 -0
  75. package/templates/code-analysis/graph.js +72 -0
  76. package/templates/code-analysis/index.js +18 -0
  77. package/templates/code-analysis/nodes/analyze-ticket-node.js +204 -0
  78. package/templates/code-analysis/nodes/create-pr-node.js +175 -0
  79. package/templates/code-analysis/nodes/finalize-node.js +118 -0
  80. package/templates/code-analysis/nodes/generate-code-node.js +425 -0
  81. package/templates/code-analysis/nodes/generate-test-cases-node.js +376 -0
  82. package/templates/code-analysis/nodes/services/prMetaService.js +86 -0
  83. package/templates/code-analysis/nodes/setup-node.js +142 -0
  84. package/templates/code-analysis/prompts/analyze-ticket.md +181 -0
  85. package/templates/code-analysis/prompts/generate-code.md +33 -0
  86. package/templates/code-analysis/prompts/generate-test-cases.md +110 -0
  87. package/templates/code-analysis/state.js +40 -0
  88. package/templates/code-implementation/graph.js +35 -0
  89. package/templates/code-implementation/index.js +7 -0
  90. package/templates/code-implementation/state.js +14 -0
  91. package/templates/global-setup.js +56 -0
  92. package/templates/index.js +94 -0
  93. package/templates/register-nodes.js +24 -0
@@ -0,0 +1,522 @@
1
+ /**
2
+ * Node Schema Parser
3
+ * Extracts exact return schema from node execute functions using AST analysis
4
+ * Provides 100% accurate state variable information for the Variable Inspector
5
+ */
6
+
7
+ import * as acorn from 'acorn';
8
+ import * as walk from 'acorn-walk';
9
+
10
+ /**
11
+ * Parse a node definition and extract its state output schema
12
+ * @param {Object} nodeDefinition - Node definition object with execute function
13
+ * @returns {Object} Schema describing the node's state output
14
+ */
15
+ export function parseNodeSchema(nodeDefinition) {
16
+ if (!nodeDefinition || typeof nodeDefinition.execute !== 'function') {
17
+ return createUnknownSchema('No execute function found');
18
+ }
19
+
20
+ const code = nodeDefinition.execute.toString();
21
+ return parseExecuteSchema(code);
22
+ }
23
+
24
+ /**
25
+ * Parse execute function code and extract return schema
26
+ * @param {string} code - Execute function source code
27
+ * @returns {Object} Schema describing the return value
28
+ */
29
+ export function parseExecuteSchema(code) {
30
+ if (!code || typeof code !== 'string') {
31
+ return createUnknownSchema('Invalid code');
32
+ }
33
+
34
+ try {
35
+ // Handle different function syntaxes:
36
+ // 1. async (state) => { ... }
37
+ // 2. async function(state) { ... }
38
+ // 3. async execute(state) { ... } (method syntax)
39
+ let wrappedCode;
40
+ const trimmed = code.trimStart();
41
+
42
+ if (trimmed.startsWith('async execute(')) {
43
+ // async execute(state) { ... } -> async (state) => { ... }
44
+ wrappedCode = `const __fn = async ${trimmed.substring('async execute'.length).replace(/^\(/, '(').replace(/\)\s*\{/, ') => {')}`;
45
+ } else if (trimmed.startsWith('execute(')) {
46
+ // execute(state) { ... } -> (state) => { ... }
47
+ wrappedCode = `const __fn = ${trimmed.substring('execute'.length).replace(/\)\s*\{/, ') => {')}`;
48
+ } else {
49
+ wrappedCode = `const __fn = ${trimmed}`;
50
+ }
51
+
52
+ const ast = acorn.parse(wrappedCode, {
53
+ ecmaVersion: 'latest',
54
+ sourceType: 'module',
55
+ allowAwaitOutsideModules: true
56
+ });
57
+
58
+ const returnSchemas = [];
59
+ const variableScopes = new Map();
60
+
61
+ // First pass: collect variable declarations
62
+ walk.simple(ast, {
63
+ VariableDeclarator(node) {
64
+ if (node.id.type === 'Identifier' && node.init) {
65
+ variableScopes.set(node.id.name, node.init);
66
+ }
67
+ }
68
+ });
69
+
70
+ // Second pass: extract return statements
71
+ walk.simple(ast, {
72
+ ReturnStatement(node) {
73
+ if (node.argument) {
74
+ const schema = extractSchemaFromNode(node.argument, variableScopes);
75
+ returnSchemas.push(schema);
76
+ }
77
+ }
78
+ });
79
+
80
+ if (returnSchemas.length === 0) {
81
+ return createUnknownSchema('No return statements found');
82
+ }
83
+
84
+ // Merge all return schemas (handles multiple return paths)
85
+ return mergeSchemas(returnSchemas);
86
+ } catch (error) {
87
+ return createUnknownSchema(`Parse error: ${error.message}`);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Extract schema from an AST node
93
+ * @param {Object} node - AST node
94
+ * @param {Map} variableScopes - Map of variable names to their init nodes
95
+ * @returns {Object} Schema object
96
+ */
97
+ function extractSchemaFromNode(node, variableScopes) {
98
+ switch (node.type) {
99
+ case 'ObjectExpression':
100
+ return extractObjectSchema(node, variableScopes);
101
+
102
+ case 'ArrayExpression':
103
+ return extractArraySchema(node, variableScopes);
104
+
105
+ case 'Literal':
106
+ return extractLiteralSchema(node);
107
+
108
+ case 'Identifier':
109
+ return resolveIdentifier(node.name, variableScopes);
110
+
111
+ case 'MemberExpression':
112
+ return extractMemberSchema(node);
113
+
114
+ case 'ConditionalExpression': {
115
+ // condition ? consequent : alternate
116
+ const consequent = extractSchemaFromNode(node.consequent, variableScopes);
117
+ const alternate = extractSchemaFromNode(node.alternate, variableScopes);
118
+ return mergeSchemas([consequent, alternate]);
119
+ }
120
+
121
+ case 'LogicalExpression':
122
+ // a || b, a && b
123
+ if (node.operator === '||') {
124
+ return extractSchemaFromNode(node.left, variableScopes);
125
+ }
126
+ return extractSchemaFromNode(node.right, variableScopes);
127
+
128
+ case 'CallExpression':
129
+ return extractCallSchema(node);
130
+
131
+ case 'AwaitExpression':
132
+ return extractSchemaFromNode(node.argument, variableScopes);
133
+
134
+ case 'TemplateLiteral':
135
+ return { type: 'string' };
136
+
137
+ case 'UnaryExpression':
138
+ if (node.operator === '!') return { type: 'boolean' };
139
+ return { type: 'unknown' };
140
+
141
+ case 'BinaryExpression':
142
+ if (['===', '!==', '==', '!=', '<', '>', '<=', '>='].includes(node.operator)) {
143
+ return { type: 'boolean' };
144
+ }
145
+ if (['+', '-', '*', '/', '%'].includes(node.operator)) {
146
+ return { type: 'number' };
147
+ }
148
+ return { type: 'unknown' };
149
+
150
+ default:
151
+ return { type: 'unknown', astType: node.type };
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Extract schema from an object expression
157
+ */
158
+ function extractObjectSchema(node, variableScopes) {
159
+ const properties = {};
160
+
161
+ for (const prop of node.properties) {
162
+ if (prop.type === 'SpreadElement') {
163
+ // Handle spread: { ...variable } or { ...(condition && { prop }) }
164
+ const spreadSchema = extractSpreadSchema(prop.argument, variableScopes);
165
+ if (spreadSchema.properties) {
166
+ for (const [key, value] of Object.entries(spreadSchema.properties)) {
167
+ properties[key] = { ...value, optional: true };
168
+ }
169
+ }
170
+ } else if (prop.type === 'Property') {
171
+ const key = getPropertyKey(prop);
172
+ if (key) {
173
+ const valueSchema = extractSchemaFromNode(prop.value, variableScopes);
174
+ properties[key] = valueSchema;
175
+ }
176
+ }
177
+ }
178
+
179
+ return {
180
+ type: 'object',
181
+ properties
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Extract schema from spread expressions
187
+ */
188
+ function extractSpreadSchema(node, variableScopes) {
189
+ // Handle: ...(condition && { prop: value })
190
+ if (node.type === 'LogicalExpression' && node.operator === '&&') {
191
+ return extractSchemaFromNode(node.right, variableScopes);
192
+ }
193
+
194
+ // Handle: ...variable
195
+ if (node.type === 'Identifier') {
196
+ return resolveIdentifier(node.name, variableScopes);
197
+ }
198
+
199
+ // Handle: ...obj.property
200
+ if (node.type === 'MemberExpression') {
201
+ return extractMemberSchema(node);
202
+ }
203
+
204
+ return extractSchemaFromNode(node, variableScopes);
205
+ }
206
+
207
+ /**
208
+ * Extract schema from array expression
209
+ */
210
+ function extractArraySchema(node, variableScopes) {
211
+ if (node.elements.length === 0) {
212
+ return { type: 'array', items: { type: 'unknown' } };
213
+ }
214
+
215
+ // Infer item type from first element
216
+ const firstElement = node.elements[0];
217
+ if (firstElement) {
218
+ const itemSchema = extractSchemaFromNode(firstElement, variableScopes);
219
+ return { type: 'array', items: itemSchema };
220
+ }
221
+
222
+ return { type: 'array', items: { type: 'unknown' } };
223
+ }
224
+
225
+ /**
226
+ * Extract schema from literal values
227
+ */
228
+ function extractLiteralSchema(node) {
229
+ const value = node.value;
230
+
231
+ if (value === null) return { type: 'null' };
232
+ if (typeof value === 'boolean') return { type: 'boolean', value };
233
+ if (typeof value === 'number') return { type: 'number' };
234
+ if (typeof value === 'string') return { type: 'string' };
235
+
236
+ return { type: 'unknown' };
237
+ }
238
+
239
+ /**
240
+ * Resolve an identifier to its schema
241
+ */
242
+ function resolveIdentifier(name, variableScopes) {
243
+ // Check if we have the variable's init expression
244
+ const initNode = variableScopes.get(name);
245
+ if (initNode) {
246
+ return extractSchemaFromNode(initNode, variableScopes);
247
+ }
248
+
249
+ // Common known identifiers
250
+ const knownTypes = {
251
+ 'true': { type: 'boolean', value: true },
252
+ 'false': { type: 'boolean', value: false },
253
+ 'null': { type: 'null' },
254
+ 'undefined': { type: 'undefined' }
255
+ };
256
+
257
+ if (knownTypes[name]) {
258
+ return knownTypes[name];
259
+ }
260
+
261
+ // Return reference type with the variable name for context
262
+ return { type: 'reference', name, label: camelToTitle(name) };
263
+ }
264
+
265
+ /**
266
+ * Extract schema from member expression (e.g., obj.prop)
267
+ */
268
+ function extractMemberSchema(node) {
269
+ const path = getMemberPath(node);
270
+ const lastPart = path[path.length - 1];
271
+
272
+ // Infer type from common property names
273
+ const typeHints = {
274
+ 'length': { type: 'number' },
275
+ 'trim': { type: 'string' },
276
+ 'toString': { type: 'string' },
277
+ 'toISOString': { type: 'string' },
278
+ 'success': { type: 'boolean' },
279
+ 'error': { type: 'string' },
280
+ 'message': { type: 'string' },
281
+ 'url': { type: 'string' },
282
+ 'path': { type: 'string' },
283
+ 'name': { type: 'string' },
284
+ 'id': { type: 'string' },
285
+ 'count': { type: 'number' },
286
+ 'total': { type: 'number' },
287
+ 'timestamp': { type: 'string' }
288
+ };
289
+
290
+ return typeHints[lastPart] || {
291
+ type: 'reference',
292
+ path: path.join('.'),
293
+ label: camelToTitle(lastPart)
294
+ };
295
+ }
296
+
297
+ /**
298
+ * Get the full path of a member expression
299
+ */
300
+ function getMemberPath(node) {
301
+ const parts = [];
302
+
303
+ function traverse(n) {
304
+ if (n.type === 'MemberExpression') {
305
+ traverse(n.object);
306
+ if (n.property.type === 'Identifier') {
307
+ parts.push(n.property.name);
308
+ } else if (n.property.type === 'Literal') {
309
+ parts.push(String(n.property.value));
310
+ }
311
+ } else if (n.type === 'Identifier') {
312
+ parts.push(n.name);
313
+ }
314
+ }
315
+
316
+ traverse(node);
317
+ return parts;
318
+ }
319
+
320
+ /**
321
+ * Extract schema from call expressions
322
+ */
323
+ function extractCallSchema(node) {
324
+ const callee = node.callee;
325
+
326
+ // Handle common method calls
327
+ if (callee.type === 'MemberExpression') {
328
+ const method = callee.property?.name;
329
+
330
+ const methodTypes = {
331
+ 'trim': { type: 'string' },
332
+ 'toString': { type: 'string' },
333
+ 'toISOString': { type: 'string' },
334
+ 'toJSON': { type: 'object' },
335
+ 'join': { type: 'string' },
336
+ 'map': { type: 'array' },
337
+ 'filter': { type: 'array' },
338
+ 'slice': { type: 'array' },
339
+ 'concat': { type: 'array' },
340
+ 'split': { type: 'array' },
341
+ 'stringify': { type: 'string' },
342
+ 'parse': { type: 'object' }
343
+ };
344
+
345
+ if (methodTypes[method]) {
346
+ return methodTypes[method];
347
+ }
348
+ }
349
+
350
+ // Handle constructor calls
351
+ if (callee.type === 'Identifier') {
352
+ const constructorTypes = {
353
+ 'Date': { type: 'object', label: 'Date' },
354
+ 'Array': { type: 'array' },
355
+ 'Object': { type: 'object' },
356
+ 'String': { type: 'string' },
357
+ 'Number': { type: 'number' },
358
+ 'Boolean': { type: 'boolean' }
359
+ };
360
+
361
+ if (constructorTypes[callee.name]) {
362
+ return constructorTypes[callee.name];
363
+ }
364
+ }
365
+
366
+ return { type: 'unknown', call: true };
367
+ }
368
+
369
+ /**
370
+ * Get property key from a Property node
371
+ */
372
+ function getPropertyKey(prop) {
373
+ if (prop.key.type === 'Identifier') {
374
+ return prop.key.name;
375
+ }
376
+ if (prop.key.type === 'Literal') {
377
+ return String(prop.key.value);
378
+ }
379
+ return null;
380
+ }
381
+
382
+ /**
383
+ * Merge multiple schemas (for multiple return paths)
384
+ */
385
+ function mergeSchemas(schemas) {
386
+ if (schemas.length === 0) {
387
+ return createUnknownSchema('Empty schemas');
388
+ }
389
+
390
+ if (schemas.length === 1) {
391
+ return schemas[0];
392
+ }
393
+
394
+ // If all schemas are objects, merge their properties
395
+ const objectSchemas = schemas.filter(s => s.type === 'object');
396
+ if (objectSchemas.length === schemas.length) {
397
+ const mergedProperties = {};
398
+
399
+ for (const schema of objectSchemas) {
400
+ if (schema.properties) {
401
+ for (const [key, value] of Object.entries(schema.properties)) {
402
+ if (mergedProperties[key]) {
403
+ // Property exists in multiple returns - mark as potentially optional
404
+ mergedProperties[key] = {
405
+ ...mergeSchemas([mergedProperties[key], value]),
406
+ optional: true
407
+ };
408
+ } else {
409
+ mergedProperties[key] = value;
410
+ }
411
+ }
412
+ }
413
+ }
414
+
415
+ return {
416
+ type: 'object',
417
+ properties: mergedProperties
418
+ };
419
+ }
420
+
421
+ // Mixed types - return union
422
+ return {
423
+ type: 'union',
424
+ schemas
425
+ };
426
+ }
427
+
428
+ /**
429
+ * Create an unknown schema with reason
430
+ */
431
+ function createUnknownSchema(reason) {
432
+ return {
433
+ type: 'unknown',
434
+ reason
435
+ };
436
+ }
437
+
438
+ /**
439
+ * Convert camelCase to Title Case
440
+ */
441
+ function camelToTitle(str) {
442
+ if (!str) return '';
443
+ return str
444
+ .replace(/([A-Z])/g, ' $1')
445
+ .replace(/^./, s => s.toUpperCase())
446
+ .trim();
447
+ }
448
+
449
+ /**
450
+ * Parse all nodes from a module and extract their schemas
451
+ * @param {Object} nodeDefinitions - Object mapping node names to definitions
452
+ * @returns {Object} Object mapping node names to schemas
453
+ */
454
+ export function parseAllNodeSchemas(nodeDefinitions) {
455
+ const schemas = {};
456
+
457
+ for (const [name, definition] of Object.entries(nodeDefinitions)) {
458
+ schemas[name] = {
459
+ name: definition.name || name,
460
+ hasOutputSchema: !!definition.outputSchema,
461
+ stateSchema: parseNodeSchema(definition)
462
+ };
463
+ }
464
+
465
+ return schemas;
466
+ }
467
+
468
+ /**
469
+ * Convert parsed schema to display-friendly format for Variable Inspector
470
+ * @param {Object} schema - Parsed schema
471
+ * @param {string} prefix - Path prefix
472
+ * @returns {Array} Array of { path, type, label, optional } objects
473
+ */
474
+ export function schemaToVariables(schema, prefix = '') {
475
+ const variables = [];
476
+
477
+ // Handle union types by merging all object schemas
478
+ if (schema.type === 'union' && schema.schemas) {
479
+ const mergedProps = {};
480
+ for (const subSchema of schema.schemas) {
481
+ if (subSchema.type === 'object' && subSchema.properties) {
482
+ for (const [key, propSchema] of Object.entries(subSchema.properties)) {
483
+ if (!mergedProps[key]) {
484
+ mergedProps[key] = { ...propSchema, optional: true };
485
+ }
486
+ }
487
+ }
488
+ }
489
+ return schemaToVariables({ type: 'object', properties: mergedProps }, prefix);
490
+ }
491
+
492
+ if (schema.type === 'object' && schema.properties) {
493
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
494
+ const path = prefix ? `${prefix}.${key}` : key;
495
+ const label = propSchema.label || camelToTitle(key);
496
+
497
+ variables.push({
498
+ path,
499
+ type: propSchema.type,
500
+ label,
501
+ optional: propSchema.optional || false
502
+ });
503
+
504
+ // Recurse into nested objects
505
+ if (propSchema.type === 'object' && propSchema.properties) {
506
+ variables.push(...schemaToVariables(propSchema, path));
507
+ }
508
+
509
+ // Recurse into union types within properties
510
+ if (propSchema.type === 'union' && propSchema.schemas) {
511
+ const nestedVars = schemaToVariables(propSchema, path);
512
+ for (const v of nestedVars) {
513
+ if (!variables.find(existing => existing.path === v.path)) {
514
+ variables.push(v);
515
+ }
516
+ }
517
+ }
518
+ }
519
+ }
520
+
521
+ return variables;
522
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Post-process events.json to enrich with trace data
3
+ * This extracts ACTUAL element text from trace for multi-language support
4
+ */
5
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { TraceTextEnricher } from '../enrichment/trace-text-enricher.js';
8
+
9
+ /**
10
+ * Enrich events.json with actual text from trace
11
+ * @param {string} sessionPath - Path to session directory (e.g., .../execute_live)
12
+ * @returns {Promise<{enriched: number, failed: number}>}
13
+ */
14
+ export async function postProcessEvents(sessionPath) {
15
+ const eventsPath = join(sessionPath, 'events.json');
16
+ const enrichedPath = join(sessionPath, 'events-enriched.json');
17
+
18
+ if (!existsSync(eventsPath)) {
19
+ console.log('[PostProcess] No events.json found');
20
+ return { enriched: 0, failed: 0 };
21
+ }
22
+
23
+ try {
24
+ const events = JSON.parse(readFileSync(eventsPath, 'utf-8'));
25
+ const enricher = new TraceTextEnricher();
26
+
27
+ let enriched = 0;
28
+ let failed = 0;
29
+
30
+ for (const event of events) {
31
+ try {
32
+ const enrichedData = await enricher.enrich(event, { sessionPath });
33
+ if (enrichedData) {
34
+ event.enrichedData = { ...(event.enrichedData || {}), ...enrichedData };
35
+ enriched++;
36
+ }
37
+ } catch (e) {
38
+ console.log(`[PostProcess] Failed to enrich event ${event.id}: ${e.message}`);
39
+ failed++;
40
+ }
41
+ }
42
+
43
+ // Save enriched events (backup original)
44
+ if (enriched > 0) {
45
+ writeFileSync(enrichedPath, JSON.stringify(events, null, 2));
46
+ writeFileSync(eventsPath, JSON.stringify(events, null, 2));
47
+ console.log(`[PostProcess] ✅ Enriched ${enriched} events (${failed} failed)`);
48
+ }
49
+
50
+ return { enriched, failed };
51
+ } catch (e) {
52
+ console.log(`[PostProcess] ❌ Failed to post-process events: ${e.message}`);
53
+ return { enriched: 0, failed: 0 };
54
+ }
55
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * ResultHandler - Generic post-execution logic for any workflow.
3
+ * Saves results, logs outcomes. Subclass and override onNodeSaved()
4
+ * for workflow-specific post-processing (event enrichment, video, etc.)
5
+ */
6
+
7
+ import { writeFileSync, existsSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { logger } from './logger.js';
10
+
11
+ export class ResultHandler {
12
+ /**
13
+ * Save execution title to session.
14
+ * Searches state for the first node output containing a 'title' or 'result' string field.
15
+ */
16
+ static saveTitle(result, _cwd) {
17
+ const sessionPath = result.state.sessionPath;
18
+ if (!sessionPath) return;
19
+
20
+ const title = ResultHandler._findInState(result.state, 'title')
21
+ || ResultHandler._findInState(result.state, 'result');
22
+ if (!title || typeof title !== 'string') return;
23
+
24
+ try {
25
+ const titlePath = join(sessionPath, 'title.txt');
26
+ writeFileSync(titlePath, title, 'utf-8');
27
+ logger.info(`Saved title to session: "${title}"`);
28
+ } catch (err) {
29
+ console.warn('⚠️ Could not save title file:', err.message);
30
+ }
31
+ }
32
+
33
+ static _findInState(state, key) {
34
+ for (const [, value] of Object.entries(state)) {
35
+ if (value && typeof value === 'object' && value[key] !== undefined) {
36
+ return value[key];
37
+ }
38
+ }
39
+ return undefined;
40
+ }
41
+
42
+ /**
43
+ * Save execution data for each node that produced actions.
44
+ * Writes result.json then calls this.onNodeSaved() for subclass processing.
45
+ */
46
+ static async saveExecutionData(result) {
47
+ const sessionPath = result.state.sessionPath;
48
+ if (!sessionPath) return;
49
+
50
+ for (const [nodeName, output] of Object.entries(result.state)) {
51
+ if (!output || typeof output !== 'object' || !output.actions) continue;
52
+
53
+ try {
54
+ const nodeFolder = join(sessionPath, nodeName);
55
+ if (!existsSync(nodeFolder)) continue;
56
+
57
+ const executionDataPath = join(nodeFolder, 'result.json');
58
+ writeFileSync(executionDataPath, JSON.stringify(output, null, 2), 'utf-8');
59
+ logger.info(`Saved execution data to ${nodeName} folder`);
60
+
61
+ await this.onNodeSaved(nodeFolder, output);
62
+ } catch (err) {
63
+ console.warn(`⚠️ Could not save execution data for ${nodeName}:`, err.message);
64
+ }
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Hook for subclasses -- called after result.json is written for each node.
70
+ * Override to add enrichment, video processing, etc.
71
+ */
72
+ static async onNodeSaved(_nodeFolder, _executionData) {}
73
+
74
+ /**
75
+ * Log workflow result. Inspects all node outputs for success/failure indicators.
76
+ */
77
+ static logResult(result, outputPath) {
78
+ const allOutputs = Object.entries(result.state)
79
+ .filter(([, v]) => v && typeof v === 'object' && v.success !== undefined);
80
+
81
+ const allPassed = allOutputs.length > 0 && allOutputs.every(([, v]) => v.success);
82
+ const anyFailed = allOutputs.some(([, v]) => v.success === false);
83
+
84
+ if (allPassed) {
85
+ logger.info('Workflow completed successfully.');
86
+ if (outputPath) logger.info(`Output: ${outputPath}`);
87
+ } else if (anyFailed) {
88
+ const failedNodes = allOutputs.filter(([, v]) => !v.success).map(([k]) => k);
89
+ logger.info(`Workflow completed with failures in: ${failedNodes.join(', ')}`);
90
+ if (outputPath) logger.info(`Output: ${outputPath}`);
91
+ }
92
+
93
+ return allPassed;
94
+ }
95
+
96
+ static handle(result, cwd, outputPath) {
97
+ this.saveTitle(result, cwd);
98
+ this.saveExecutionData(result);
99
+ const success = this.logResult(result, outputPath);
100
+ return success;
101
+ }
102
+ }