blokctl 0.2.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 (169) hide show
  1. package/dist/commands/build/index.d.ts +2 -0
  2. package/dist/commands/build/index.js +210 -0
  3. package/dist/commands/config/index.d.ts +1 -0
  4. package/dist/commands/config/index.js +46 -0
  5. package/dist/commands/cost/index.d.ts +1 -0
  6. package/dist/commands/cost/index.js +74 -0
  7. package/dist/commands/create/node.d.ts +2 -0
  8. package/dist/commands/create/node.js +541 -0
  9. package/dist/commands/create/project.d.ts +2 -0
  10. package/dist/commands/create/project.js +941 -0
  11. package/dist/commands/create/utils/Examples.d.ts +39 -0
  12. package/dist/commands/create/utils/Examples.js +983 -0
  13. package/dist/commands/create/workflow.d.ts +2 -0
  14. package/dist/commands/create/workflow.js +109 -0
  15. package/dist/commands/deploy/index.d.ts +2 -0
  16. package/dist/commands/deploy/index.js +176 -0
  17. package/dist/commands/dev/index.d.ts +2 -0
  18. package/dist/commands/dev/index.js +190 -0
  19. package/dist/commands/generate/GenerationAnalytics.d.ts +61 -0
  20. package/dist/commands/generate/GenerationAnalytics.js +162 -0
  21. package/dist/commands/generate/GenerationAnalytics.test.d.ts +1 -0
  22. package/dist/commands/generate/GenerationAnalytics.test.js +407 -0
  23. package/dist/commands/generate/NodeFileWriter.d.ts +5 -0
  24. package/dist/commands/generate/NodeFileWriter.js +240 -0
  25. package/dist/commands/generate/NodeGenerator.d.ts +20 -0
  26. package/dist/commands/generate/NodeGenerator.js +181 -0
  27. package/dist/commands/generate/NodeGenerator.test.d.ts +1 -0
  28. package/dist/commands/generate/NodeGenerator.test.js +101 -0
  29. package/dist/commands/generate/PromptVersioning.d.ts +25 -0
  30. package/dist/commands/generate/PromptVersioning.js +71 -0
  31. package/dist/commands/generate/PromptVersioning.test.d.ts +1 -0
  32. package/dist/commands/generate/PromptVersioning.test.js +120 -0
  33. package/dist/commands/generate/RegisterNode.d.ts +3 -0
  34. package/dist/commands/generate/RegisterNode.js +37 -0
  35. package/dist/commands/generate/RuntimeGenerator.d.ts +40 -0
  36. package/dist/commands/generate/RuntimeGenerator.js +369 -0
  37. package/dist/commands/generate/RuntimeGenerator.test.d.ts +1 -0
  38. package/dist/commands/generate/RuntimeGenerator.test.js +553 -0
  39. package/dist/commands/generate/TriggerGenerator.d.ts +22 -0
  40. package/dist/commands/generate/TriggerGenerator.js +220 -0
  41. package/dist/commands/generate/TriggerGenerator.test.d.ts +1 -0
  42. package/dist/commands/generate/TriggerGenerator.test.js +209 -0
  43. package/dist/commands/generate/WorkflowGenerator.d.ts +20 -0
  44. package/dist/commands/generate/WorkflowGenerator.js +131 -0
  45. package/dist/commands/generate/WorkflowGenerator.test.d.ts +1 -0
  46. package/dist/commands/generate/WorkflowGenerator.test.js +77 -0
  47. package/dist/commands/generate/e2e/NodeGenerator.e2e.test.d.ts +1 -0
  48. package/dist/commands/generate/e2e/NodeGenerator.e2e.test.js +216 -0
  49. package/dist/commands/generate/e2e/RuntimeGenerator.e2e.test.d.ts +1 -0
  50. package/dist/commands/generate/e2e/RuntimeGenerator.e2e.test.js +759 -0
  51. package/dist/commands/generate/e2e/TriggerGenerator.e2e.test.d.ts +1 -0
  52. package/dist/commands/generate/e2e/TriggerGenerator.e2e.test.js +295 -0
  53. package/dist/commands/generate/e2e/WorkflowGenerator.e2e.test.d.ts +1 -0
  54. package/dist/commands/generate/e2e/WorkflowGenerator.e2e.test.js +353 -0
  55. package/dist/commands/generate/index.d.ts +1 -0
  56. package/dist/commands/generate/index.js +418 -0
  57. package/dist/commands/generate/prompts/create-fn-node.system.d.ts +5 -0
  58. package/dist/commands/generate/prompts/create-fn-node.system.js +256 -0
  59. package/dist/commands/generate/prompts/create-node-manifest.system.d.ts +4 -0
  60. package/dist/commands/generate/prompts/create-node-manifest.system.js +41 -0
  61. package/dist/commands/generate/prompts/create-node.system.d.ts +5 -0
  62. package/dist/commands/generate/prompts/create-node.system.js +114 -0
  63. package/dist/commands/generate/prompts/create-readme.system.d.ts +4 -0
  64. package/dist/commands/generate/prompts/create-readme.system.js +83 -0
  65. package/dist/commands/generate/prompts/create-runtime.system.d.ts +5 -0
  66. package/dist/commands/generate/prompts/create-runtime.system.js +284 -0
  67. package/dist/commands/generate/prompts/create-trigger.system.d.ts +5 -0
  68. package/dist/commands/generate/prompts/create-trigger.system.js +293 -0
  69. package/dist/commands/generate/prompts/create-workflow.system.d.ts +5 -0
  70. package/dist/commands/generate/prompts/create-workflow.system.js +476 -0
  71. package/dist/commands/generate/prompts/register-node.system.d.ts +4 -0
  72. package/dist/commands/generate/prompts/register-node.system.js +26 -0
  73. package/dist/commands/generate/validators/CompilationValidator.d.ts +9 -0
  74. package/dist/commands/generate/validators/CompilationValidator.js +86 -0
  75. package/dist/commands/generate/validators/CompilationValidator.test.d.ts +1 -0
  76. package/dist/commands/generate/validators/CompilationValidator.test.js +161 -0
  77. package/dist/commands/generate/validators/NodeValidator.d.ts +18 -0
  78. package/dist/commands/generate/validators/NodeValidator.js +217 -0
  79. package/dist/commands/generate/validators/NodeValidator.test.d.ts +1 -0
  80. package/dist/commands/generate/validators/NodeValidator.test.js +281 -0
  81. package/dist/commands/generate/validators/WorkflowValidator.d.ts +6 -0
  82. package/dist/commands/generate/validators/WorkflowValidator.js +301 -0
  83. package/dist/commands/generate/validators/WorkflowValidator.test.d.ts +1 -0
  84. package/dist/commands/generate/validators/WorkflowValidator.test.js +647 -0
  85. package/dist/commands/generate/validators/index.d.ts +4 -0
  86. package/dist/commands/generate/validators/index.js +2 -0
  87. package/dist/commands/graph/index.d.ts +1 -0
  88. package/dist/commands/graph/index.js +69 -0
  89. package/dist/commands/install/index.d.ts +1 -0
  90. package/dist/commands/install/index.js +4 -0
  91. package/dist/commands/install/node.d.ts +4 -0
  92. package/dist/commands/install/node.js +136 -0
  93. package/dist/commands/install/workflow.d.ts +4 -0
  94. package/dist/commands/install/workflow.js +62 -0
  95. package/dist/commands/login/index.d.ts +2 -0
  96. package/dist/commands/login/index.js +77 -0
  97. package/dist/commands/logout/index.d.ts +2 -0
  98. package/dist/commands/logout/index.js +20 -0
  99. package/dist/commands/marketplace/runtime.d.ts +54 -0
  100. package/dist/commands/marketplace/runtime.js +350 -0
  101. package/dist/commands/migrate/index.d.ts +1 -0
  102. package/dist/commands/migrate/index.js +14 -0
  103. package/dist/commands/migrate/node.d.ts +2 -0
  104. package/dist/commands/migrate/node.js +110 -0
  105. package/dist/commands/monitor/index.d.ts +1 -0
  106. package/dist/commands/monitor/index.js +28 -0
  107. package/dist/commands/monitor/monitor-component.d.ts +1 -0
  108. package/dist/commands/monitor/monitor-component.js +271 -0
  109. package/dist/commands/monitor/static/index.html +2124 -0
  110. package/dist/commands/monitor/static-web-server.d.ts +1 -0
  111. package/dist/commands/monitor/static-web-server.js +89 -0
  112. package/dist/commands/profile/index.d.ts +1 -0
  113. package/dist/commands/profile/index.js +112 -0
  114. package/dist/commands/publish/index.d.ts +1 -0
  115. package/dist/commands/publish/index.js +4 -0
  116. package/dist/commands/publish/node.d.ts +4 -0
  117. package/dist/commands/publish/node.js +231 -0
  118. package/dist/commands/publish/workflow.d.ts +4 -0
  119. package/dist/commands/publish/workflow.js +165 -0
  120. package/dist/commands/search/docs.d.ts +17 -0
  121. package/dist/commands/search/docs.js +179 -0
  122. package/dist/commands/search/index.d.ts +1 -0
  123. package/dist/commands/search/index.js +5 -0
  124. package/dist/commands/search/indexer.d.ts +10 -0
  125. package/dist/commands/search/indexer.js +265 -0
  126. package/dist/commands/search/nodes.d.ts +4 -0
  127. package/dist/commands/search/nodes.js +101 -0
  128. package/dist/commands/search/workflow.d.ts +4 -0
  129. package/dist/commands/search/workflow.js +100 -0
  130. package/dist/commands/trace/index.d.ts +1 -0
  131. package/dist/commands/trace/index.js +26 -0
  132. package/dist/commands/trace/startStudio.d.ts +8 -0
  133. package/dist/commands/trace/startStudio.js +116 -0
  134. package/dist/index.d.ts +17 -0
  135. package/dist/index.js +186 -0
  136. package/dist/services/commander.d.ts +9 -0
  137. package/dist/services/commander.js +20 -0
  138. package/dist/services/constants.d.ts +1 -0
  139. package/dist/services/constants.js +3 -0
  140. package/dist/services/local-token-manager.d.ts +14 -0
  141. package/dist/services/local-token-manager.js +99 -0
  142. package/dist/services/non-interactive.d.ts +5 -0
  143. package/dist/services/non-interactive.js +30 -0
  144. package/dist/services/package-manager.d.ts +35 -0
  145. package/dist/services/package-manager.js +111 -0
  146. package/dist/services/posthog.d.ts +31 -0
  147. package/dist/services/posthog.js +159 -0
  148. package/dist/services/registry-manager.d.ts +9 -0
  149. package/dist/services/registry-manager.js +26 -0
  150. package/dist/services/runtime-detector.d.ts +23 -0
  151. package/dist/services/runtime-detector.js +181 -0
  152. package/dist/services/runtime-setup.d.ts +36 -0
  153. package/dist/services/runtime-setup.js +250 -0
  154. package/dist/services/utils.d.ts +2 -0
  155. package/dist/services/utils.js +29 -0
  156. package/dist/services/workflow-loader.d.ts +30 -0
  157. package/dist/services/workflow-loader.js +46 -0
  158. package/dist/studio-dist/assets/charts-Dso0hPUR.js +68 -0
  159. package/dist/studio-dist/assets/graph-CsV2nWGn.js +23 -0
  160. package/dist/studio-dist/assets/icons-zP8LLgPh.js +311 -0
  161. package/dist/studio-dist/assets/index-CLyEkXMx.css +1 -0
  162. package/dist/studio-dist/assets/index-CNXFX_ar.js +27 -0
  163. package/dist/studio-dist/assets/react-vendor--Eh9ivFN.js +17 -0
  164. package/dist/studio-dist/assets/tanstack-query-CiM1U6F5.js +1 -0
  165. package/dist/studio-dist/assets/tanstack-router-Btjy0MKq.js +25 -0
  166. package/dist/studio-dist/assets/tanstack-table-DhwRvuH2.js +22 -0
  167. package/dist/studio-dist/favicon.svg +5 -0
  168. package/dist/studio-dist/index.html +21 -0
  169. package/package.json +75 -0
@@ -0,0 +1,220 @@
1
+ import * as fs from "node:fs";
2
+ import { createOpenAI } from "@ai-sdk/openai";
3
+ import { generateText } from "ai";
4
+ import { GenerationAnalytics } from "./GenerationAnalytics.js";
5
+ import { getVersionStamp, registerPromptContent } from "./PromptVersioning.js";
6
+ import createTriggerSystemPrompt from "./prompts/create-trigger.system.js";
7
+ import * as CompilationValidator from "./validators/CompilationValidator.js";
8
+ export default class TriggerGenerator {
9
+ MAX_VALIDATION_ATTEMPTS = 3;
10
+ async generateTrigger(triggerName, triggerType, userPrompt, apiKey, update = false, existingTriggerPath) {
11
+ const analytics = GenerationAnalytics.getInstance();
12
+ const getElapsed = analytics.startTimer();
13
+ const promptVersion = getVersionStamp("create-trigger");
14
+ const openai = createOpenAI({
15
+ compatibility: "strict",
16
+ apiKey: apiKey,
17
+ });
18
+ let prompt = createTriggerSystemPrompt.prompt;
19
+ registerPromptContent("create-trigger", prompt);
20
+ if (update && existingTriggerPath) {
21
+ const existingContent = fs.readFileSync(existingTriggerPath, "utf8");
22
+ prompt = `${createTriggerSystemPrompt.updatePrompt}\n\n${existingContent}`;
23
+ }
24
+ const enhancedPrompt = this.buildEnhancedPrompt(userPrompt, triggerType, triggerName);
25
+ let attempts = 0;
26
+ let generatedCode = "";
27
+ let validationErrors = [];
28
+ let validationWarnings = [];
29
+ let isValid = false;
30
+ const allErrors = [];
31
+ while (attempts < this.MAX_VALIDATION_ATTEMPTS && !isValid) {
32
+ attempts++;
33
+ let finalPrompt = enhancedPrompt;
34
+ if (attempts > 1 && validationErrors.length > 0) {
35
+ finalPrompt = this.createFeedbackPrompt(enhancedPrompt, generatedCode, validationErrors);
36
+ }
37
+ const { text } = await generateText({
38
+ model: openai("gpt-4o"),
39
+ system: prompt,
40
+ prompt: finalPrompt,
41
+ temperature: 0.2,
42
+ });
43
+ generatedCode = text.replace(/^```typescript\s*([\s\S]*?)\s*```$/gm, "$1").trim();
44
+ const compilationResult = CompilationValidator.validateCode(generatedCode, `${triggerName}-trigger.ts`);
45
+ validationErrors = compilationResult.errors;
46
+ validationWarnings = compilationResult.warnings;
47
+ isValid = compilationResult.success;
48
+ if (isValid) {
49
+ const structureResult = this.validateTriggerStructure(generatedCode);
50
+ if (!structureResult.valid) {
51
+ isValid = false;
52
+ validationErrors.push(...structureResult.errors);
53
+ validationWarnings.push(...structureResult.warnings);
54
+ }
55
+ }
56
+ allErrors.push(...validationErrors);
57
+ if (!isValid && attempts < this.MAX_VALIDATION_ATTEMPTS) {
58
+ console.log(`⚠️ Trigger validation failed (attempt ${attempts}/${this.MAX_VALIDATION_ATTEMPTS}). Retrying with feedback...`);
59
+ }
60
+ }
61
+ const durationMs = getElapsed();
62
+ analytics.recordEvent({
63
+ type: "trigger",
64
+ subtype: triggerType,
65
+ name: triggerName,
66
+ success: isValid,
67
+ attempts,
68
+ durationMs,
69
+ errors: allErrors,
70
+ promptVersion,
71
+ });
72
+ return {
73
+ triggerName,
74
+ triggerType,
75
+ userPrompt,
76
+ code: generatedCode,
77
+ validationResult: {
78
+ valid: isValid,
79
+ errors: validationErrors,
80
+ warnings: validationWarnings,
81
+ attempts,
82
+ promptVersion,
83
+ durationMs,
84
+ },
85
+ };
86
+ }
87
+ validateTriggerStructure(code) {
88
+ const errors = [];
89
+ const warnings = [];
90
+ if (!code.includes("extends TriggerBase") && !code.includes("extends") && !code.includes("TriggerBase")) {
91
+ errors.push("Trigger must extend TriggerBase");
92
+ }
93
+ if (!code.includes("TriggerBase") && !code.includes("@blok/runner")) {
94
+ errors.push("Missing TriggerBase import from @blok/runner");
95
+ }
96
+ if (!code.includes("loadNodes")) {
97
+ errors.push("Missing loadNodes() method");
98
+ }
99
+ if (!code.includes("loadWorkflows")) {
100
+ errors.push("Missing loadWorkflows() method");
101
+ }
102
+ if (!code.includes("createContext")) {
103
+ errors.push("Must use this.createContext() to create workflow context");
104
+ }
105
+ if (!code.includes(".request") ||
106
+ (!code.includes("ctx.request") && !code.includes("context.request") && !code.includes(".request ="))) {
107
+ warnings.push("Should populate ctx.request with event data");
108
+ }
109
+ if (!code.includes("export default")) {
110
+ warnings.push("Consider exporting the trigger class as default");
111
+ }
112
+ if (code.includes("constructor") && !code.includes("super()")) {
113
+ errors.push("Constructor must call super()");
114
+ }
115
+ return {
116
+ valid: errors.length === 0,
117
+ errors,
118
+ warnings,
119
+ };
120
+ }
121
+ buildEnhancedPrompt(userPrompt, triggerType, triggerName) {
122
+ const parts = [
123
+ `Create a trigger named "${triggerName}" of type "${triggerType}" with the following requirements:`,
124
+ "",
125
+ userPrompt,
126
+ "",
127
+ `IMPORTANT: This trigger MUST handle the "${triggerType}" trigger type.`,
128
+ ];
129
+ switch (triggerType) {
130
+ case "queue":
131
+ parts.push("The trigger should connect to a message queue broker (Kafka, RabbitMQ, SQS, or Redis/BullMQ).");
132
+ parts.push("It should consume messages, match them to workflows, create context, and execute workflows.");
133
+ parts.push("Include proper ack/nack handling and dead letter queue support.");
134
+ break;
135
+ case "pubsub":
136
+ parts.push("The trigger should connect to a pub/sub provider (GCP, AWS SNS, or Azure Service Bus).");
137
+ parts.push("It should subscribe to topics, receive messages, and execute matching workflows.");
138
+ parts.push("Include proper acknowledgment and message filtering.");
139
+ break;
140
+ case "cron":
141
+ parts.push("The trigger should schedule recurring jobs using cron expressions.");
142
+ parts.push("Include timezone support, overlap prevention, and manual trigger capability.");
143
+ break;
144
+ case "webhook":
145
+ parts.push("The trigger should expose HTTP endpoints for receiving webhook events.");
146
+ parts.push("Include signature verification for common providers (GitHub, Stripe, Shopify).");
147
+ parts.push("Support event type filtering with wildcards.");
148
+ break;
149
+ case "websocket":
150
+ parts.push("The trigger should create a WebSocket server for real-time bidirectional communication.");
151
+ parts.push("Include room/channel management, connection tracking, and authentication middleware.");
152
+ break;
153
+ case "sse":
154
+ parts.push("The trigger should create SSE (Server-Sent Events) endpoints.");
155
+ parts.push("Include channel management, event formatting, Last-Event-ID replay support, and heartbeat.");
156
+ break;
157
+ default:
158
+ parts.push(`Implement a custom trigger for the "${triggerType}" event source.`);
159
+ break;
160
+ }
161
+ return parts.join("\n");
162
+ }
163
+ createFeedbackPrompt(originalPrompt, previousCode, errors) {
164
+ const analyzedErrors = errors.map((err, i) => {
165
+ const guidance = this.getSemanticGuidance(err);
166
+ return `${i + 1}. ${err}${guidance ? `\n Fix: ${guidance}` : ""}`;
167
+ });
168
+ const feedback = [
169
+ originalPrompt,
170
+ "",
171
+ "❌ The previous generation had validation errors. Here's what went wrong and how to fix it:",
172
+ "",
173
+ ...analyzedErrors,
174
+ "",
175
+ "Previous code:",
176
+ "```typescript",
177
+ previousCode,
178
+ "```",
179
+ "",
180
+ "Please fix ALL the errors listed above and regenerate the complete trigger code.",
181
+ "Make sure to:",
182
+ "- Extend TriggerBase from @blok/runner",
183
+ "- Include loadNodes() and loadWorkflows() methods",
184
+ "- Use this.createContext() to create Context objects",
185
+ "- Call super() in the constructor",
186
+ "- Populate ctx.request with event data before executing workflows",
187
+ "- Export the class as default: export default class <Name>Trigger extends TriggerBase",
188
+ "- Use Object.keys(workflowModel.trigger) to extract trigger config",
189
+ ].join("\n");
190
+ return feedback;
191
+ }
192
+ getSemanticGuidance(error) {
193
+ const errorLower = error.toLowerCase();
194
+ if (errorLower.includes("triggerbase") && (errorLower.includes("extend") || errorLower.includes("missing"))) {
195
+ return "import { TriggerBase } from '@blok/runner'; class MyTrigger extends TriggerBase { ... }";
196
+ }
197
+ if (errorLower.includes("loadnodes") && errorLower.includes("missing")) {
198
+ return "Add: private loadNodes(): void { this.nodeMap.nodes = new NodeMap(); ... }";
199
+ }
200
+ if (errorLower.includes("loadworkflows") && errorLower.includes("missing")) {
201
+ return "Add: private loadWorkflows(): void { this.nodeMap.workflows = workflows; }";
202
+ }
203
+ if (errorLower.includes("createcontext")) {
204
+ return "Use this.createContext(undefined, workflowModel.path) to create context for each workflow execution";
205
+ }
206
+ if (errorLower.includes("super()")) {
207
+ return "Add super(); as the first line in constructor()";
208
+ }
209
+ if (errorLower.includes("ctx.request") || errorLower.includes("event data")) {
210
+ return "Set ctx.request = { body: messageData, headers: {}, query: {}, params: {} } before executing workflow";
211
+ }
212
+ if (errorLower.includes("cannot find module") || errorLower.includes("module not found")) {
213
+ return "Use '@blok/runner' for TriggerBase, Runner, NodeMap and '@blok/shared' for Context, DefaultLogger";
214
+ }
215
+ if (errorLower.includes("export") && errorLower.includes("default")) {
216
+ return "Use: export default class <Name>Trigger extends TriggerBase { ... }";
217
+ }
218
+ return null;
219
+ }
220
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,209 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import TriggerGenerator from "./TriggerGenerator.js";
3
+ describe("TriggerGenerator", () => {
4
+ describe("validateTriggerStructure (via reflection)", () => {
5
+ const generator = new TriggerGenerator();
6
+ const validate = (code) => generator.validateTriggerStructure(code);
7
+ it("should pass for valid trigger code", () => {
8
+ const validCode = `
9
+ import TriggerBase from "@blok/runner/TriggerBase";
10
+ import { NodeMap } from "@blok/runner";
11
+
12
+ export default class MyTrigger extends TriggerBase {
13
+ constructor() {
14
+ super();
15
+ this.loadNodes();
16
+ this.loadWorkflows();
17
+ }
18
+
19
+ private loadNodes() {
20
+ // load nodes
21
+ }
22
+
23
+ private loadWorkflows() {
24
+ // load workflows
25
+ }
26
+
27
+ async start() {
28
+ const ctx = this.createContext();
29
+ ctx.request = { body: {} };
30
+ }
31
+ }
32
+ `;
33
+ const result = validate(validCode);
34
+ expect(result.valid).toBe(true);
35
+ expect(result.errors.length).toBe(0);
36
+ });
37
+ it("should fail for trigger not extending TriggerBase", () => {
38
+ const code = `
39
+ export default class MyTrigger {
40
+ constructor() {
41
+ this.loadNodes();
42
+ this.loadWorkflows();
43
+ }
44
+
45
+ private loadNodes() {}
46
+ private loadWorkflows() {}
47
+
48
+ async start() {
49
+ const ctx = this.createContext();
50
+ }
51
+ }
52
+ `;
53
+ const result = validate(code);
54
+ expect(result.valid).toBe(false);
55
+ expect(result.errors.some((e) => e.includes("TriggerBase"))).toBe(true);
56
+ });
57
+ it("should fail for trigger missing loadNodes", () => {
58
+ const code = `
59
+ import TriggerBase from "@blok/runner/TriggerBase";
60
+
61
+ export default class MyTrigger extends TriggerBase {
62
+ constructor() {
63
+ super();
64
+ this.loadWorkflows();
65
+ }
66
+
67
+ private loadWorkflows() {}
68
+
69
+ async start() {
70
+ const ctx = this.createContext();
71
+ }
72
+ }
73
+ `;
74
+ const result = validate(code);
75
+ expect(result.valid).toBe(false);
76
+ expect(result.errors.some((e) => e.includes("loadNodes"))).toBe(true);
77
+ });
78
+ it("should fail for trigger missing loadWorkflows", () => {
79
+ const code = `
80
+ import TriggerBase from "@blok/runner/TriggerBase";
81
+
82
+ export default class MyTrigger extends TriggerBase {
83
+ constructor() {
84
+ super();
85
+ this.loadNodes();
86
+ }
87
+
88
+ private loadNodes() {}
89
+
90
+ async start() {
91
+ const ctx = this.createContext();
92
+ }
93
+ }
94
+ `;
95
+ const result = validate(code);
96
+ expect(result.valid).toBe(false);
97
+ expect(result.errors.some((e) => e.includes("loadWorkflows"))).toBe(true);
98
+ });
99
+ it("should fail for trigger missing createContext", () => {
100
+ const code = `
101
+ import TriggerBase from "@blok/runner/TriggerBase";
102
+
103
+ export default class MyTrigger extends TriggerBase {
104
+ constructor() {
105
+ super();
106
+ this.loadNodes();
107
+ this.loadWorkflows();
108
+ }
109
+
110
+ private loadNodes() {}
111
+ private loadWorkflows() {}
112
+
113
+ async start() {
114
+ // does not build context
115
+ }
116
+ }
117
+ `;
118
+ const result = validate(code);
119
+ expect(result.valid).toBe(false);
120
+ expect(result.errors.some((e) => e.includes("createContext"))).toBe(true);
121
+ });
122
+ it("should fail for constructor without super()", () => {
123
+ const code = `
124
+ import TriggerBase from "@blok/runner/TriggerBase";
125
+
126
+ export default class MyTrigger extends TriggerBase {
127
+ constructor() {
128
+ this.loadNodes();
129
+ this.loadWorkflows();
130
+ }
131
+
132
+ private loadNodes() {}
133
+ private loadWorkflows() {}
134
+
135
+ async start() {
136
+ const ctx = this.createContext();
137
+ }
138
+ }
139
+ `;
140
+ const result = validate(code);
141
+ expect(result.valid).toBe(false);
142
+ expect(result.errors.some((e) => e.includes("super()"))).toBe(true);
143
+ });
144
+ it("should warn when request data is not populated on context", () => {
145
+ const code = `
146
+ import TriggerBase from "@blok/runner/TriggerBase";
147
+
148
+ export default class MyTrigger extends TriggerBase {
149
+ constructor() {
150
+ super();
151
+ this.loadNodes();
152
+ this.loadWorkflows();
153
+ }
154
+
155
+ private loadNodes() {}
156
+ private loadWorkflows() {}
157
+
158
+ async start() {
159
+ const c = this.createContext();
160
+ // does not set request data
161
+ }
162
+ }
163
+ `;
164
+ const result = validate(code);
165
+ expect(result.valid).toBe(true);
166
+ expect(result.warnings.some((w) => w.includes("ctx.request") || w.includes("event data"))).toBe(true);
167
+ });
168
+ });
169
+ describe("buildEnhancedPrompt (via reflection)", () => {
170
+ const generator = new TriggerGenerator();
171
+ const buildPrompt = (userPrompt, triggerType, triggerName) => generator.buildEnhancedPrompt(userPrompt, triggerType, triggerName);
172
+ it("should include trigger type in prompt", () => {
173
+ const result = buildPrompt("Process user events", "queue", "user-queue");
174
+ expect(result).toContain("queue");
175
+ expect(result).toContain("user-queue");
176
+ expect(result).toContain("Process user events");
177
+ });
178
+ it("should include queue-specific guidance for queue type", () => {
179
+ const result = buildPrompt("Process messages", "queue", "my-queue");
180
+ expect(result).toContain("message queue broker");
181
+ expect(result).toContain("ack/nack");
182
+ });
183
+ it("should include pubsub-specific guidance for pubsub type", () => {
184
+ const result = buildPrompt("Process events", "pubsub", "my-pubsub");
185
+ expect(result).toContain("pub/sub provider");
186
+ });
187
+ it("should include cron-specific guidance for cron type", () => {
188
+ const result = buildPrompt("Run daily", "cron", "daily-job");
189
+ expect(result).toContain("cron expressions");
190
+ expect(result).toContain("timezone");
191
+ });
192
+ it("should include webhook-specific guidance for webhook type", () => {
193
+ const result = buildPrompt("Handle webhooks", "webhook", "github-webhook");
194
+ expect(result).toContain("signature verification");
195
+ });
196
+ it("should include websocket-specific guidance for websocket type", () => {
197
+ const result = buildPrompt("Real-time chat", "websocket", "chat-ws");
198
+ expect(result).toContain("WebSocket server");
199
+ });
200
+ it("should include sse-specific guidance for sse type", () => {
201
+ const result = buildPrompt("Stream events", "sse", "event-stream");
202
+ expect(result).toContain("SSE");
203
+ });
204
+ it("should handle custom trigger types", () => {
205
+ const result = buildPrompt("Custom trigger", "custom", "my-custom");
206
+ expect(result).toContain("custom");
207
+ });
208
+ });
209
+ });
@@ -0,0 +1,20 @@
1
+ export type WorkflowInformation = {
2
+ workflowName: string;
3
+ userPrompt: string;
4
+ json: string;
5
+ triggerType: string;
6
+ validationResult?: {
7
+ valid: boolean;
8
+ errors: string[];
9
+ warnings: string[];
10
+ attempts: number;
11
+ promptVersion?: string;
12
+ durationMs?: number;
13
+ };
14
+ };
15
+ export default class WorkflowGenerator {
16
+ private readonly MAX_VALIDATION_ATTEMPTS;
17
+ generateWorkflow(workflowName: string, userPrompt: string, apiKey: string, triggerType: string, update?: boolean, existingWorkflowPath?: string): Promise<WorkflowInformation>;
18
+ private buildEnhancedPrompt;
19
+ private createFeedbackPrompt;
20
+ }
@@ -0,0 +1,131 @@
1
+ import * as fs from "node:fs";
2
+ import { createOpenAI } from "@ai-sdk/openai";
3
+ import { generateText } from "ai";
4
+ import { GenerationAnalytics } from "./GenerationAnalytics.js";
5
+ import { getVersionStamp, registerPromptContent } from "./PromptVersioning.js";
6
+ import createWorkflowSystemPrompt from "./prompts/create-workflow.system.js";
7
+ import * as WorkflowValidator from "./validators/WorkflowValidator.js";
8
+ export default class WorkflowGenerator {
9
+ MAX_VALIDATION_ATTEMPTS = 3;
10
+ async generateWorkflow(workflowName, userPrompt, apiKey, triggerType, update = false, existingWorkflowPath) {
11
+ const analytics = GenerationAnalytics.getInstance();
12
+ const getElapsed = analytics.startTimer();
13
+ const promptVersion = getVersionStamp("create-workflow");
14
+ const openai = createOpenAI({
15
+ compatibility: "strict",
16
+ apiKey: apiKey,
17
+ });
18
+ let prompt = createWorkflowSystemPrompt.prompt;
19
+ registerPromptContent("create-workflow", prompt);
20
+ if (update && existingWorkflowPath) {
21
+ const existingContent = fs.readFileSync(existingWorkflowPath, "utf8");
22
+ prompt = `${createWorkflowSystemPrompt.updatePrompt}\n\n${existingContent}`;
23
+ }
24
+ const enhancedPrompt = this.buildEnhancedPrompt(userPrompt, triggerType, workflowName);
25
+ let attempts = 0;
26
+ let generatedJson = "";
27
+ let validationErrors = [];
28
+ let validationWarnings = [];
29
+ let isValid = false;
30
+ const allErrors = [];
31
+ while (attempts < this.MAX_VALIDATION_ATTEMPTS && !isValid) {
32
+ attempts++;
33
+ let finalPrompt = enhancedPrompt;
34
+ if (attempts > 1 && validationErrors.length > 0) {
35
+ finalPrompt = this.createFeedbackPrompt(enhancedPrompt, generatedJson, validationErrors);
36
+ }
37
+ const { text } = await generateText({
38
+ model: openai("gpt-4o"),
39
+ system: prompt,
40
+ prompt: finalPrompt,
41
+ temperature: 0.2,
42
+ });
43
+ generatedJson = text.replace(/^```json\s*([\s\S]*?)\s*```$/gm, "$1").trim();
44
+ const result = WorkflowValidator.validateWorkflow(generatedJson);
45
+ validationErrors = result.errors;
46
+ validationWarnings = result.warnings;
47
+ isValid = result.valid;
48
+ allErrors.push(...validationErrors);
49
+ if (!isValid && attempts < this.MAX_VALIDATION_ATTEMPTS) {
50
+ console.log(`⚠️ Workflow validation failed (attempt ${attempts}/${this.MAX_VALIDATION_ATTEMPTS}). Retrying with feedback...`);
51
+ }
52
+ }
53
+ const durationMs = getElapsed();
54
+ analytics.recordEvent({
55
+ type: "workflow",
56
+ subtype: triggerType,
57
+ name: workflowName,
58
+ success: isValid,
59
+ attempts,
60
+ durationMs,
61
+ errors: allErrors,
62
+ promptVersion,
63
+ });
64
+ return {
65
+ workflowName,
66
+ userPrompt,
67
+ json: generatedJson,
68
+ triggerType,
69
+ validationResult: {
70
+ valid: isValid,
71
+ errors: validationErrors,
72
+ warnings: validationWarnings,
73
+ attempts,
74
+ promptVersion,
75
+ durationMs,
76
+ },
77
+ };
78
+ }
79
+ buildEnhancedPrompt(userPrompt, triggerType, workflowName) {
80
+ const parts = [`Create a workflow named "${workflowName}" with the following requirements:`, "", userPrompt];
81
+ if (triggerType && triggerType !== "auto") {
82
+ parts.push("");
83
+ parts.push(`IMPORTANT: This workflow MUST use the "${triggerType}" trigger type.`);
84
+ switch (triggerType) {
85
+ case "http":
86
+ parts.push("Configure the HTTP trigger with appropriate method, path, and accept settings.");
87
+ break;
88
+ case "queue":
89
+ parts.push("Configure the queue trigger with appropriate provider, topic, and consumer group settings.");
90
+ break;
91
+ case "pubsub":
92
+ parts.push("Configure the pub/sub trigger with appropriate provider, topic, and subscription settings.");
93
+ break;
94
+ case "cron":
95
+ parts.push("Configure the cron trigger with appropriate schedule expression and timezone.");
96
+ break;
97
+ case "webhook":
98
+ parts.push("Configure the webhook trigger with appropriate source, events, and optional secret.");
99
+ break;
100
+ case "websocket":
101
+ parts.push("Configure the WebSocket trigger with appropriate events and connection settings.");
102
+ break;
103
+ case "sse":
104
+ parts.push("Configure the SSE trigger with appropriate events and channel settings.");
105
+ break;
106
+ }
107
+ }
108
+ return parts.join("\n");
109
+ }
110
+ createFeedbackPrompt(originalPrompt, previousJson, errors) {
111
+ const feedback = [
112
+ originalPrompt,
113
+ "",
114
+ "❌ The previous generation had validation errors:",
115
+ ...errors.map((err, i) => `${i + 1}. ${err}`),
116
+ "",
117
+ "Previous workflow JSON:",
118
+ "```json",
119
+ previousJson,
120
+ "```",
121
+ "",
122
+ "Please fix these errors and regenerate the workflow JSON. Common fixes:",
123
+ "- Ensure every step name has a matching entry in the nodes object",
124
+ "- Ensure the trigger has exactly one trigger type with valid configuration",
125
+ "- Ensure condition expressions use ctx.* properties",
126
+ "- Ensure all conditional branches have name, node, and type fields",
127
+ "- Add an else branch to conditional routing for error handling",
128
+ ].join("\n");
129
+ return feedback;
130
+ }
131
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,77 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import WorkflowGenerator from "./WorkflowGenerator.js";
3
+ describe("WorkflowGenerator", () => {
4
+ describe("buildEnhancedPrompt (via reflection)", () => {
5
+ const generator = new WorkflowGenerator();
6
+ const buildPrompt = (userPrompt, triggerType, workflowName) => generator.buildEnhancedPrompt(userPrompt, triggerType, workflowName);
7
+ it("should include workflow name and user prompt", () => {
8
+ const result = buildPrompt("Fetch user data from API", "http", "user-api");
9
+ expect(result).toContain("user-api");
10
+ expect(result).toContain("Fetch user data from API");
11
+ });
12
+ it("should include http trigger guidance for http type", () => {
13
+ const result = buildPrompt("Handle REST requests", "http", "rest-api");
14
+ expect(result).toContain("http");
15
+ expect(result).toContain("HTTP trigger");
16
+ });
17
+ it("should include queue trigger guidance for queue type", () => {
18
+ const result = buildPrompt("Process messages", "queue", "message-processor");
19
+ expect(result).toContain("queue");
20
+ expect(result).toContain("queue trigger");
21
+ });
22
+ it("should include pubsub trigger guidance for pubsub type", () => {
23
+ const result = buildPrompt("Process events", "pubsub", "event-handler");
24
+ expect(result).toContain("pubsub");
25
+ expect(result).toContain("pub/sub trigger");
26
+ });
27
+ it("should include cron trigger guidance for cron type", () => {
28
+ const result = buildPrompt("Run hourly job", "cron", "hourly-job");
29
+ expect(result).toContain("cron");
30
+ expect(result).toContain("cron trigger");
31
+ });
32
+ it("should include webhook trigger guidance for webhook type", () => {
33
+ const result = buildPrompt("Handle GitHub events", "webhook", "github-handler");
34
+ expect(result).toContain("webhook");
35
+ expect(result).toContain("webhook trigger");
36
+ });
37
+ it("should include websocket trigger guidance for websocket type", () => {
38
+ const result = buildPrompt("Real-time updates", "websocket", "realtime");
39
+ expect(result).toContain("websocket");
40
+ expect(result).toContain("WebSocket trigger");
41
+ });
42
+ it("should include sse trigger guidance for sse type", () => {
43
+ const result = buildPrompt("Stream updates", "sse", "event-stream");
44
+ expect(result).toContain("sse");
45
+ expect(result).toContain("SSE trigger");
46
+ });
47
+ it("should skip trigger-specific guidance for auto type", () => {
48
+ const result = buildPrompt("Do something", "auto", "auto-workflow");
49
+ expect(result).not.toContain("MUST use the");
50
+ });
51
+ });
52
+ describe("createFeedbackPrompt (via reflection)", () => {
53
+ const generator = new WorkflowGenerator();
54
+ const createFeedback = (originalPrompt, previousJson, errors) => generator.createFeedbackPrompt(originalPrompt, previousJson, errors);
55
+ it("should include original prompt in feedback", () => {
56
+ const result = createFeedback("Build a user API", "{}", ["Missing name field"]);
57
+ expect(result).toContain("Build a user API");
58
+ });
59
+ it("should include all validation errors", () => {
60
+ const errors = ["Missing name field", "Invalid trigger type", "Step has no matching node"];
61
+ const result = createFeedback("Test", "{}", errors);
62
+ for (const error of errors) {
63
+ expect(result).toContain(error);
64
+ }
65
+ });
66
+ it("should include previous JSON for reference", () => {
67
+ const previousJson = '{"name": "test", "version": "1.0.0"}';
68
+ const result = createFeedback("Test", previousJson, ["Error"]);
69
+ expect(result).toContain(previousJson);
70
+ });
71
+ it("should include common fix suggestions", () => {
72
+ const result = createFeedback("Test", "{}", ["Missing node entry"]);
73
+ expect(result).toContain("step name has a matching entry");
74
+ expect(result).toContain("else branch");
75
+ });
76
+ });
77
+ });