@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,78 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Function Bridge — wraps plain handler functions in an MCP server.
4
+ *
5
+ * Usage: node function-bridge.js <modulePath> <skillId>
6
+ *
7
+ * 1. Dynamically imports the user's module (which calls functionSkill(),
8
+ * populating the handler registry).
9
+ * 2. Reads the registered handlers and tool schemas.
10
+ * 3. Starts an MCP server over stdio so agent strategies can talk to it.
11
+ */
12
+
13
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
+ import { z } from 'zod';
16
+
17
+ const [modulePath, skillId] = process.argv.slice(2);
18
+
19
+ if (!modulePath || !skillId) {
20
+ console.error('Usage: node function-bridge.js <modulePath> <skillId>');
21
+ process.exit(1);
22
+ }
23
+
24
+ await import(modulePath);
25
+
26
+ const { getHandlers } = await import('./function-skill-registry.js');
27
+ const registration = getHandlers(skillId);
28
+
29
+ if (!registration) {
30
+ console.error(`No handlers registered for skill "${skillId}". Did the module call functionSkill()?`);
31
+ process.exit(1);
32
+ }
33
+
34
+ const { handlers, tools } = registration;
35
+
36
+ function jsonSchemaToZod(schema) {
37
+ if (!schema || schema.type !== 'object') return z.object({});
38
+
39
+ const shape = {};
40
+ for (const [key, prop] of Object.entries(schema.properties || {})) {
41
+ let field;
42
+ switch (prop.type) {
43
+ case 'number': case 'integer': field = z.number(); break;
44
+ case 'boolean': field = z.boolean(); break;
45
+ case 'array': field = z.array(z.any()); break;
46
+ default: field = z.string(); break;
47
+ }
48
+ if (prop.description) field = field.describe(prop.description);
49
+ if (!schema.required?.includes(key)) field = field.optional();
50
+ shape[key] = field;
51
+ }
52
+ return z.object(shape);
53
+ }
54
+
55
+ const server = new McpServer(
56
+ { name: `zibby-fn-${skillId}`, version: '1.0.0' },
57
+ { capabilities: { tools: {} } }
58
+ );
59
+
60
+ for (const tool of tools) {
61
+ const zodSchema = jsonSchemaToZod(tool.input_schema);
62
+ server.registerTool(
63
+ tool.name,
64
+ { title: tool.name, description: tool.description || '', inputSchema: zodSchema },
65
+ async (args) => {
66
+ try {
67
+ const result = await handlers[tool.name](args);
68
+ const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
69
+ return { content: [{ type: 'text', text }] };
70
+ } catch (err) {
71
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
72
+ }
73
+ }
74
+ );
75
+ }
76
+
77
+ const transport = new StdioServerTransport();
78
+ await server.connect(transport);
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Function Skill Handler Registry
3
+ *
4
+ * Shared in-memory store for function skill handlers. When a module calls
5
+ * functionSkill(), handlers are stored here. When the bridge imports that
6
+ * module, it reads the handlers from this registry.
7
+ *
8
+ * This works because Node.js module caching ensures both the user's module
9
+ * and the bridge resolve to the same instance of this registry.
10
+ */
11
+
12
+ const _registry = new Map();
13
+
14
+ export function registerHandlers(skillId, handlers, tools) {
15
+ _registry.set(skillId, { handlers, tools });
16
+ }
17
+
18
+ export function getHandlers(skillId) {
19
+ return _registry.get(skillId) || null;
20
+ }
@@ -0,0 +1,342 @@
1
+ /* global require */
2
+ import { WorkflowGraph } from './graph.js';
3
+ import { getNodeImpl, hasNode } from './node-registry.js';
4
+ import { resolveNodeTools } from './tool-resolver.js';
5
+ import { logger } from '../utils/logger.js';
6
+
7
+ export function compileGraph(config, options = {}) {
8
+ const { nodes, edges, nodeConfigs = {} } = config;
9
+
10
+ if (!Array.isArray(nodes) || nodes.length === 0) {
11
+ throw new CompilationError('Graph must have at least one node');
12
+ }
13
+ if (!Array.isArray(edges)) {
14
+ throw new CompilationError('Graph edges must be an array');
15
+ }
16
+
17
+ const graph = new WorkflowGraph(options);
18
+
19
+ if (options.stateSchema) {
20
+ graph.setStateSchema(options.stateSchema);
21
+ }
22
+
23
+ const decisionNodeIds = new Set();
24
+ const nodeMap = new Map();
25
+ const resolvedToolsMap = {};
26
+
27
+ // Pass 1: Identify decision nodes (visual-only, not executable)
28
+ for (const node of nodes) {
29
+ const nodeType = resolveNodeType(node);
30
+ nodeMap.set(node.id, { ...node, resolvedType: nodeType });
31
+ if (nodeType === 'decision') {
32
+ decisionNodeIds.add(node.id);
33
+ }
34
+ }
35
+
36
+ // Pass 2: Add executable nodes and resolve tools
37
+ for (const [nodeId, node] of nodeMap) {
38
+ if (decisionNodeIds.has(nodeId)) continue;
39
+
40
+ const nodeType = node.resolvedType;
41
+ const nodeConfig = nodeConfigs[nodeId] || {};
42
+ const userToolIds = nodeConfig.tools;
43
+ const resolved = resolveNodeTools(nodeType, userToolIds);
44
+ if (resolved) {
45
+ resolvedToolsMap[nodeId] = resolved;
46
+ }
47
+
48
+ const nodeOptions = {};
49
+ if (nodeConfig.prompt) {
50
+ nodeOptions.prompt = nodeConfig.prompt;
51
+ }
52
+
53
+ const isRegistered = hasNode(nodeType);
54
+ logger.debug(`[compiler] Node "${nodeId}" type="${nodeType}" registered=${isRegistered} hasCustomCode=${!!nodeConfig.customCode} hasExecuteCode=${!!nodeConfig.executeCode}`);
55
+
56
+ if (nodeConfig.customCode && !isRegistered) {
57
+ logger.debug(`[compiler] → using customCode (unregistered node)`);
58
+ graph.addNode(nodeId, wrapCustomCode(nodeId, nodeConfig.customCode, nodeConfig), nodeOptions);
59
+ graph.setNodeType(nodeId, nodeType);
60
+ } else if (isRegistered) {
61
+ logger.debug(`[compiler] → using registered implementation`);
62
+ const impl = getNodeImpl(nodeType);
63
+ if (impl.factory) {
64
+ graph.addNode(nodeId, impl.create(nodeId, {
65
+ ...nodeConfig,
66
+ resolvedTools: resolved
67
+ }), nodeOptions);
68
+ } else {
69
+ graph.addNode(nodeId, impl, nodeOptions);
70
+ }
71
+ graph.setNodeType(nodeId, nodeType);
72
+ } else if (nodeConfig.executeCode) {
73
+ logger.debug(`[compiler] → using executeCode (fallback)`);
74
+ graph.addNode(nodeId, wrapCustomCode(nodeId, nodeConfig.executeCode, nodeConfig), nodeOptions);
75
+ graph.setNodeType(nodeId, nodeType);
76
+ } else {
77
+ throw new CompilationError(
78
+ `Unknown node type "${nodeType}" for node "${nodeId}". ` +
79
+ `Did you forget to register it?`
80
+ );
81
+ }
82
+ }
83
+
84
+ graph.resolvedToolsMap = resolvedToolsMap;
85
+
86
+ // Pass 3: Determine entry point (node with no incoming non-decision edges)
87
+ const incomingTargets = new Set();
88
+ for (const edge of edges) {
89
+ if (!decisionNodeIds.has(edge.target)) {
90
+ incomingTargets.add(edge.target);
91
+ }
92
+ }
93
+ const entryNode = nodes.find(
94
+ n => !decisionNodeIds.has(n.id) && !incomingTargets.has(n.id)
95
+ );
96
+ if (!entryNode) {
97
+ throw new CompilationError('Could not determine entry point: no node without incoming edges found');
98
+ }
99
+ graph.setEntryPoint(entryNode.id);
100
+
101
+ // Pass 4: Wire edges, collapsing decision nodes into addConditionalEdges
102
+ const edgesBySource = groupBy(edges, 'source');
103
+
104
+ for (const edge of edges) {
105
+ const sourceIsDecision = decisionNodeIds.has(edge.source);
106
+ const targetIsDecision = decisionNodeIds.has(edge.target);
107
+
108
+ if (sourceIsDecision) {
109
+ // Edges FROM decision nodes are handled when we process the edge INTO the decision
110
+ continue;
111
+ }
112
+
113
+ if (targetIsDecision) {
114
+ // Edge goes into a decision node: find all outgoing edges from the decision
115
+ // and compile them into a single addConditionalEdges call
116
+ const decisionId = edge.target;
117
+ const outgoingEdges = edgesBySource.get(decisionId) || [];
118
+
119
+ if (outgoingEdges.length === 0) {
120
+ throw new CompilationError(
121
+ `Decision node "${decisionId}" has no outgoing edges`
122
+ );
123
+ }
124
+
125
+ const routeFn = compileConditionalRoutes(decisionId, outgoingEdges, decisionNodeIds);
126
+ graph.addConditionalEdges(edge.source, routeFn);
127
+ } else {
128
+ // Regular edge between two executable nodes
129
+ graph.addEdge(edge.source, edge.target);
130
+ }
131
+ }
132
+
133
+ return graph;
134
+ }
135
+
136
+ export function validateGraphConfig(config) {
137
+ const errors = [];
138
+
139
+ if (!config || typeof config !== 'object') {
140
+ return { valid: false, errors: ['Config must be a non-null object'] };
141
+ }
142
+ if (!Array.isArray(config.nodes) || config.nodes.length === 0) {
143
+ errors.push('Graph must have at least one node');
144
+ }
145
+ if (!Array.isArray(config.edges)) {
146
+ errors.push('Graph edges must be an array');
147
+ }
148
+
149
+ if (errors.length > 0) return { valid: false, errors };
150
+
151
+ const nodeConfigs = config.nodeConfigs || {};
152
+
153
+ for (const node of config.nodes) {
154
+ const nodeType = resolveNodeType(node);
155
+ if (nodeType === 'decision') continue;
156
+ if (hasNode(nodeType)) continue;
157
+
158
+ const nc = nodeConfigs[node.id] || {};
159
+ if (nc.customCode || nc.executeCode) continue;
160
+
161
+ errors.push(`Unknown node type "${nodeType}" for node "${node.id}". Register it or provide customCode/executeCode.`);
162
+ }
163
+
164
+ // Check edge references are valid
165
+ const nodeIds = new Set(config.nodes.map(n => n.id));
166
+ for (const edge of config.edges) {
167
+ if (!nodeIds.has(edge.source)) {
168
+ errors.push(`Edge references unknown source node "${edge.source}"`);
169
+ }
170
+ if (!nodeIds.has(edge.target)) {
171
+ errors.push(`Edge references unknown target node "${edge.target}"`);
172
+ }
173
+ }
174
+
175
+ // Check for exactly one entry point
176
+ const decisionIds = new Set(
177
+ config.nodes.filter(n => resolveNodeType(n) === 'decision').map(n => n.id)
178
+ );
179
+ const incomingTargets = new Set();
180
+ for (const edge of config.edges) {
181
+ if (!decisionIds.has(edge.target)) {
182
+ incomingTargets.add(edge.target);
183
+ }
184
+ }
185
+ const entryNodes = config.nodes.filter(
186
+ n => !decisionIds.has(n.id) && !incomingTargets.has(n.id)
187
+ );
188
+ if (entryNodes.length === 0) {
189
+ errors.push('No entry point found (every node has incoming edges)');
190
+ } else if (entryNodes.length > 1) {
191
+ errors.push(
192
+ `Multiple entry points found: ${entryNodes.map(n => n.id).join(', ')}. Graph must have exactly one.`
193
+ );
194
+ }
195
+
196
+ // Check decision nodes have outgoing edges with conditionalCode
197
+ for (const decisionId of decisionIds) {
198
+ const outgoing = config.edges.filter(e => e.source === decisionId);
199
+ if (outgoing.length === 0) {
200
+ errors.push(`Decision node "${decisionId}" has no outgoing edges`);
201
+ }
202
+ const hasCode = outgoing.some(
203
+ e => e.data?.conditionalCode || e.conditionalCode
204
+ );
205
+ if (!hasCode) {
206
+ errors.push(`Decision node "${decisionId}" outgoing edges have no conditionalCode`);
207
+ }
208
+ }
209
+
210
+ return { valid: errors.length === 0, errors };
211
+ }
212
+
213
+ export function extractSteps(config) {
214
+ if (!config || !Array.isArray(config.nodes)) return [];
215
+ return config.nodes
216
+ .filter(n => resolveNodeType(n) !== 'decision')
217
+ .map(n => n.id);
218
+ }
219
+
220
+ // ---- Internal helpers ----
221
+
222
+ function resolveNodeType(node) {
223
+ const raw = node.data?.nodeType || node.data?.type || node.type;
224
+ // React Flow stores UI component type (e.g. "workflowNode", "custom") — fall back to node id
225
+ if (raw === 'workflowNode' || raw === 'custom' || raw === 'default') {
226
+ return node.id;
227
+ }
228
+ return raw;
229
+ }
230
+
231
+ function groupBy(arr, key) {
232
+ const map = new Map();
233
+ for (const item of arr) {
234
+ const k = item[key];
235
+ if (!map.has(k)) map.set(k, []);
236
+ map.get(k).push(item);
237
+ }
238
+ return map;
239
+ }
240
+
241
+ function compileConditionalRoutes(decisionId, outgoingEdges, decisionNodeIds) {
242
+ // All outgoing edges from a decision share the same conditionalCode
243
+ // (the function returns a node name string to route to)
244
+ const edgeWithCode = outgoingEdges.find(
245
+ e => e.data?.conditionalCode || e.conditionalCode
246
+ );
247
+
248
+ if (!edgeWithCode) {
249
+ throw new CompilationError(
250
+ `Decision node "${decisionId}" has no conditionalCode on its outgoing edges`
251
+ );
252
+ }
253
+
254
+ const code = edgeWithCode.data?.conditionalCode || edgeWithCode.conditionalCode;
255
+ const validTargets = new Set(
256
+ outgoingEdges.map(e => e.target).filter(t => !decisionNodeIds.has(t))
257
+ );
258
+
259
+ // Compile the route function string into an actual function
260
+ // The code is expected to be: "function route(state) { ... return 'node_name'; }"
261
+ let routeFn;
262
+ try {
263
+
264
+ const factory = new Function(`return (${code})`);
265
+ const compiled = factory();
266
+
267
+ routeFn = (state) => {
268
+ const result = compiled(state);
269
+ if (!validTargets.has(result)) {
270
+ logger.warn(
271
+ `Conditional route from "${decisionId}" returned "${result}" ` +
272
+ `which is not a valid target. Valid: ${[...validTargets].join(', ')}`
273
+ );
274
+ }
275
+ return result;
276
+ };
277
+ } catch (err) {
278
+ throw new CompilationError(
279
+ `Failed to compile conditionalCode for decision "${decisionId}": ${err.message}`
280
+ );
281
+ }
282
+
283
+ return routeFn;
284
+ }
285
+
286
+ function wrapCustomCode(nodeId, codeString, nodeConfig = {}) {
287
+ let executeFn;
288
+ try {
289
+
290
+ executeFn = new Function(
291
+ 'invokeAgent', 'require', 'console',
292
+ `return (${codeString})`
293
+ );
294
+ } catch (err) {
295
+ throw new CompilationError(
296
+ `Failed to compile customCode for node "${nodeId}": ${err.message}`
297
+ );
298
+ }
299
+
300
+ const boundExecute = executeFn(
301
+ async (...args) => {
302
+ const { invokeAgent } = await import('./agents/index.js');
303
+ return invokeAgent(...args);
304
+ },
305
+ typeof require !== 'undefined' ? require : undefined,
306
+ console
307
+ );
308
+
309
+ // Reconstruct outputSchema from nodeConfig if available
310
+ let outputSchema = null;
311
+ if (nodeConfig.outputSchema) {
312
+ // Schema is already in JSON format from the enrichment
313
+ // For runtime, we need it as a validation object
314
+ // For now, we'll store the JSON schema for later use
315
+ outputSchema = nodeConfig.outputSchema.jsonSchema || nodeConfig.outputSchema;
316
+ }
317
+
318
+ return {
319
+ name: nodeId,
320
+ _isCustomCode: true,
321
+ outputSchema, // Include schema so Node constructor doesn't throw
322
+ execute: async (state) => {
323
+ try {
324
+ const result = await boundExecute(state);
325
+ return typeof result === 'object' && 'success' in result
326
+ ? result
327
+ : { success: true, output: result, raw: null };
328
+ } catch (err) {
329
+ return { success: false, error: err.message, raw: null };
330
+ }
331
+ }
332
+ };
333
+ }
334
+
335
+ class CompilationError extends Error {
336
+ constructor(message) {
337
+ super(message);
338
+ this.name = 'CompilationError';
339
+ }
340
+ }
341
+
342
+ export { CompilationError };