@pikku/inspector 0.11.1 → 0.12.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 (189) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/OPTIMIZATION-PLAN.md +195 -0
  3. package/dist/add/add-ai-agent.d.ts +2 -0
  4. package/dist/add/add-ai-agent.js +314 -0
  5. package/dist/add/add-channel.js +69 -61
  6. package/dist/add/add-cli.js +36 -18
  7. package/dist/add/add-file-with-factory.js +2 -0
  8. package/dist/add/add-functions.js +327 -59
  9. package/dist/add/add-http-route.d.ts +19 -10
  10. package/dist/add/add-http-route.js +153 -44
  11. package/dist/add/add-http-routes.d.ts +5 -0
  12. package/dist/add/add-http-routes.js +159 -0
  13. package/dist/add/add-keyed-wiring.d.ts +12 -0
  14. package/dist/add/add-keyed-wiring.js +97 -0
  15. package/dist/add/add-mcp-prompt.js +14 -9
  16. package/dist/add/add-mcp-resource.js +14 -9
  17. package/dist/add/add-middleware.d.ts +1 -4
  18. package/dist/add/add-middleware.js +364 -79
  19. package/dist/add/add-permission.d.ts +1 -1
  20. package/dist/add/add-permission.js +152 -40
  21. package/dist/add/add-queue-worker.js +18 -12
  22. package/dist/add/add-rpc-invocations.d.ts +3 -0
  23. package/dist/add/add-rpc-invocations.js +65 -25
  24. package/dist/add/add-schedule.js +11 -5
  25. package/dist/add/add-secret.d.ts +3 -0
  26. package/dist/add/add-secret.js +82 -0
  27. package/dist/add/add-trigger.d.ts +2 -0
  28. package/dist/add/add-trigger.js +87 -0
  29. package/dist/add/add-variable.d.ts +1 -0
  30. package/dist/add/add-variable.js +8 -0
  31. package/dist/add/add-workflow-graph.d.ts +7 -0
  32. package/dist/add/add-workflow-graph.js +396 -0
  33. package/dist/add/add-workflow.js +124 -26
  34. package/dist/error-codes.d.ts +16 -1
  35. package/dist/error-codes.js +21 -1
  36. package/dist/index.d.ts +9 -5
  37. package/dist/index.js +5 -2
  38. package/dist/inspector.d.ts +1 -1
  39. package/dist/inspector.js +106 -13
  40. package/dist/schema-generator.d.ts +1 -0
  41. package/dist/schema-generator.js +1 -0
  42. package/dist/types-map.js +10 -1
  43. package/dist/types.d.ts +180 -30
  44. package/dist/utils/compute-required-schemas.d.ts +4 -0
  45. package/dist/utils/compute-required-schemas.js +41 -0
  46. package/dist/utils/contract-hashes.d.ts +35 -0
  47. package/dist/utils/contract-hashes.js +202 -0
  48. package/dist/utils/custom-types-generator.d.ts +9 -0
  49. package/dist/utils/custom-types-generator.js +71 -0
  50. package/dist/utils/detect-schema-vendor.d.ts +22 -0
  51. package/dist/utils/detect-schema-vendor.js +76 -0
  52. package/dist/utils/ensure-function-metadata.d.ts +5 -2
  53. package/dist/utils/ensure-function-metadata.js +220 -6
  54. package/dist/utils/extract-function-name.d.ts +5 -16
  55. package/dist/utils/extract-function-name.js +93 -298
  56. package/dist/utils/extract-services.d.ts +2 -1
  57. package/dist/utils/extract-services.js +25 -1
  58. package/dist/utils/filter-inspector-state.js +107 -23
  59. package/dist/utils/get-property-value.d.ts +8 -2
  60. package/dist/utils/get-property-value.js +33 -4
  61. package/dist/utils/hash.d.ts +2 -0
  62. package/dist/utils/hash.js +23 -0
  63. package/dist/utils/middleware.d.ts +7 -30
  64. package/dist/utils/middleware.js +80 -66
  65. package/dist/utils/permissions.d.ts +2 -2
  66. package/dist/utils/permissions.js +10 -10
  67. package/dist/utils/post-process.d.ts +9 -10
  68. package/dist/utils/post-process.js +231 -24
  69. package/dist/utils/resolve-external-package.d.ts +12 -0
  70. package/dist/utils/resolve-external-package.js +34 -0
  71. package/dist/utils/resolve-function-types.d.ts +6 -0
  72. package/dist/utils/resolve-function-types.js +29 -0
  73. package/dist/utils/resolve-identifier.d.ts +10 -0
  74. package/dist/utils/resolve-identifier.js +36 -0
  75. package/dist/utils/resolve-versions.d.ts +2 -0
  76. package/dist/utils/resolve-versions.js +78 -0
  77. package/dist/utils/schema-generator.d.ts +9 -0
  78. package/dist/utils/schema-generator.js +209 -0
  79. package/dist/utils/serialize-inspector-state.d.ts +73 -13
  80. package/dist/utils/serialize-inspector-state.js +102 -6
  81. package/dist/utils/serialize-mcp-json.d.ts +2 -0
  82. package/dist/utils/serialize-mcp-json.js +99 -0
  83. package/dist/utils/serialize-middleware-groups-meta.d.ts +12 -0
  84. package/dist/utils/serialize-middleware-groups-meta.js +28 -0
  85. package/dist/utils/serialize-openapi-json.d.ts +85 -0
  86. package/dist/utils/serialize-openapi-json.js +151 -0
  87. package/dist/utils/serialize-permissions-groups-meta.d.ts +6 -0
  88. package/dist/utils/serialize-permissions-groups-meta.js +31 -0
  89. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.d.ts +24 -0
  90. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +830 -0
  91. package/dist/{workflow/extract-simple-workflow.d.ts → utils/workflow/dsl/extract-dsl-workflow.d.ts} +4 -2
  92. package/dist/{workflow/extract-simple-workflow.js → utils/workflow/dsl/extract-dsl-workflow.js} +572 -72
  93. package/dist/utils/workflow/dsl/index.d.ts +7 -0
  94. package/dist/utils/workflow/dsl/index.js +7 -0
  95. package/dist/{workflow → utils/workflow/dsl}/patterns.d.ts +21 -0
  96. package/dist/{workflow → utils/workflow/dsl}/patterns.js +90 -10
  97. package/dist/{workflow → utils/workflow/dsl}/validation.d.ts +2 -0
  98. package/dist/{workflow → utils/workflow/dsl}/validation.js +25 -7
  99. package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +13 -0
  100. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +318 -0
  101. package/dist/utils/workflow/graph/finalize-workflow-wires.d.ts +3 -0
  102. package/dist/utils/workflow/graph/finalize-workflow-wires.js +276 -0
  103. package/dist/utils/workflow/graph/finalize-workflows.d.ts +2 -0
  104. package/dist/utils/workflow/graph/finalize-workflows.js +75 -0
  105. package/dist/utils/workflow/graph/index.d.ts +8 -0
  106. package/dist/utils/workflow/graph/index.js +8 -0
  107. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +35 -0
  108. package/dist/utils/workflow/graph/serialize-workflow-graph.js +150 -0
  109. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +203 -0
  110. package/dist/utils/workflow/graph/workflow-graph.types.js +38 -0
  111. package/dist/visit.js +13 -2
  112. package/package.json +26 -4
  113. package/src/add/add-ai-agent.ts +468 -0
  114. package/src/add/add-channel.ts +82 -79
  115. package/src/add/add-cli.ts +49 -20
  116. package/src/add/add-file-with-factory.ts +2 -0
  117. package/src/add/add-functions.ts +429 -71
  118. package/src/add/add-http-route.ts +246 -65
  119. package/src/add/add-http-routes.ts +228 -0
  120. package/src/add/add-keyed-wiring.ts +151 -0
  121. package/src/add/add-mcp-prompt.ts +26 -15
  122. package/src/add/add-mcp-resource.ts +27 -15
  123. package/src/add/add-middleware.ts +482 -80
  124. package/src/add/add-permission.ts +199 -40
  125. package/src/add/add-queue-worker.ts +24 -19
  126. package/src/add/add-rpc-invocations.ts +78 -31
  127. package/src/add/add-schedule.ts +16 -11
  128. package/src/add/add-secret.ts +140 -0
  129. package/src/add/add-trigger.ts +154 -0
  130. package/src/add/add-variable.ts +9 -0
  131. package/src/add/add-workflow-graph.ts +522 -0
  132. package/src/add/add-workflow.ts +117 -30
  133. package/src/error-codes.ts +26 -1
  134. package/src/index.ts +27 -8
  135. package/src/inspector.ts +145 -17
  136. package/src/schema-generator.ts +1 -0
  137. package/src/types-map.ts +12 -1
  138. package/src/types.ts +192 -51
  139. package/src/utils/compute-required-schemas.ts +49 -0
  140. package/src/utils/contract-hashes.test.ts +528 -0
  141. package/src/utils/contract-hashes.ts +290 -0
  142. package/src/utils/custom-types-generator.ts +88 -0
  143. package/src/utils/detect-schema-vendor.ts +90 -0
  144. package/src/utils/ensure-function-metadata.ts +324 -7
  145. package/src/utils/extract-function-name.ts +108 -358
  146. package/src/utils/extract-services.ts +35 -2
  147. package/src/utils/filter-inspector-state.test.ts +34 -20
  148. package/src/utils/filter-inspector-state.ts +140 -31
  149. package/src/utils/get-property-value.ts +50 -5
  150. package/src/utils/hash.ts +26 -0
  151. package/src/utils/middleware.test.ts +204 -0
  152. package/src/utils/middleware.ts +129 -67
  153. package/src/utils/permissions.test.ts +35 -12
  154. package/src/utils/permissions.ts +10 -10
  155. package/src/utils/post-process.ts +283 -43
  156. package/src/utils/resolve-external-package.ts +42 -0
  157. package/src/utils/resolve-function-types.ts +42 -0
  158. package/src/utils/resolve-identifier.ts +46 -0
  159. package/src/utils/resolve-versions.test.ts +249 -0
  160. package/src/utils/resolve-versions.ts +105 -0
  161. package/src/utils/schema-generator.ts +329 -0
  162. package/src/utils/serialize-inspector-state.ts +181 -20
  163. package/src/utils/serialize-mcp-json.ts +145 -0
  164. package/src/utils/serialize-middleware-groups-meta.ts +33 -0
  165. package/src/utils/serialize-openapi-json.ts +277 -0
  166. package/src/utils/serialize-permissions-groups-meta.ts +35 -0
  167. package/src/utils/test-data/inspector-state.json +69 -66
  168. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +1104 -0
  169. package/src/{workflow/extract-simple-workflow.ts → utils/workflow/dsl/extract-dsl-workflow.ts} +678 -85
  170. package/src/utils/workflow/dsl/index.ts +11 -0
  171. package/src/{workflow → utils/workflow/dsl}/patterns.ts +108 -11
  172. package/src/{workflow → utils/workflow/dsl}/validation.ts +34 -7
  173. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +422 -0
  174. package/src/utils/workflow/graph/finalize-workflow-wires.ts +310 -0
  175. package/src/utils/workflow/graph/finalize-workflows.ts +100 -0
  176. package/src/utils/workflow/graph/index.ts +11 -0
  177. package/src/utils/workflow/graph/serialize-workflow-graph.ts +216 -0
  178. package/src/utils/workflow/graph/workflow-graph.types.ts +231 -0
  179. package/src/visit.ts +14 -2
  180. package/tsconfig.tsbuildinfo +1 -1
  181. package/dist/add/add-mcp-tool.d.ts +0 -2
  182. package/dist/add/add-mcp-tool.js +0 -81
  183. package/dist/utils/extract-service-metadata.d.ts +0 -19
  184. package/dist/utils/extract-service-metadata.js +0 -244
  185. package/dist/utils/write-service-metadata.d.ts +0 -13
  186. package/dist/utils/write-service-metadata.js +0 -37
  187. package/src/add/add-mcp-tool.ts +0 -141
  188. package/src/utils/extract-service-metadata.ts +0 -353
  189. package/src/utils/write-service-metadata.ts +0 -51
@@ -0,0 +1,11 @@
1
+ /**
2
+ * DSL (Domain Specific Language) workflow extraction exports
3
+ */
4
+ export { extractDSLWorkflow } from './extract-dsl-workflow.js'
5
+ export {
6
+ deserializeDslWorkflow,
7
+ deserializeGraphWorkflow,
8
+ deserializeAllDslWorkflows,
9
+ } from './deserialize-dsl-workflow.js'
10
+ export * from './patterns.js'
11
+ export * from './validation.js'
@@ -42,6 +42,94 @@ export function isWorkflowSleepCall(
42
42
  )
43
43
  }
44
44
 
45
+ /**
46
+ * Check if a throw statement throws WorkflowCancelledException
47
+ * Matches: throw new WorkflowCancelledException(...) or throw WorkflowCancelledException(...)
48
+ */
49
+ export function isThrowCancelException(node: ts.ThrowStatement): boolean {
50
+ const expr = node.expression
51
+ if (!expr) return false
52
+
53
+ // Check for: throw new WorkflowCancelledException(...)
54
+ if (ts.isNewExpression(expr)) {
55
+ if (ts.isIdentifier(expr.expression)) {
56
+ return expr.expression.text === 'WorkflowCancelledException'
57
+ }
58
+ }
59
+
60
+ // Check for: throw WorkflowCancelledException(...) - function call style
61
+ if (ts.isCallExpression(expr)) {
62
+ if (ts.isIdentifier(expr.expression)) {
63
+ return expr.expression.text === 'WorkflowCancelledException'
64
+ }
65
+ }
66
+
67
+ return false
68
+ }
69
+
70
+ /**
71
+ * Extract the reason string from a throw WorkflowCancelledException statement
72
+ */
73
+ export function extractCancelReason(
74
+ node: ts.ThrowStatement,
75
+ checker: ts.TypeChecker
76
+ ): string | undefined {
77
+ const expr = node.expression
78
+ if (!expr) return undefined
79
+
80
+ let args: ts.NodeArray<ts.Expression> | undefined
81
+
82
+ if (ts.isNewExpression(expr) && expr.arguments) {
83
+ args = expr.arguments
84
+ } else if (ts.isCallExpression(expr)) {
85
+ args = expr.arguments
86
+ }
87
+
88
+ if (args && args.length > 0) {
89
+ const firstArg = args[0]
90
+ if (ts.isStringLiteral(firstArg)) {
91
+ return firstArg.text
92
+ }
93
+ // For template literals or other expressions, return the source text
94
+ return firstArg.getText()
95
+ }
96
+
97
+ return undefined
98
+ }
99
+
100
+ /**
101
+ * Check if a call expression is array.filter()
102
+ */
103
+ export function isArrayFilter(node: ts.CallExpression): boolean {
104
+ if (!ts.isPropertyAccessExpression(node.expression)) {
105
+ return false
106
+ }
107
+
108
+ return node.expression.name.text === 'filter'
109
+ }
110
+
111
+ /**
112
+ * Check if a call expression is array.some()
113
+ */
114
+ export function isArraySome(node: ts.CallExpression): boolean {
115
+ if (!ts.isPropertyAccessExpression(node.expression)) {
116
+ return false
117
+ }
118
+
119
+ return node.expression.name.text === 'some'
120
+ }
121
+
122
+ /**
123
+ * Check if a call expression is array.every()
124
+ */
125
+ export function isArrayEvery(node: ts.CallExpression): boolean {
126
+ if (!ts.isPropertyAccessExpression(node.expression)) {
127
+ return false
128
+ }
129
+
130
+ return node.expression.name.text === 'every'
131
+ }
132
+
45
133
  /**
46
134
  * Check if an expression is Promise.all(array.map(...))
47
135
  */
@@ -118,6 +206,24 @@ export function isSequentialFanout(node: ts.ForOfStatement): boolean {
118
206
  return true
119
207
  }
120
208
 
209
+ /**
210
+ * Extract full source path from an expression (e.g., data.memberEmails)
211
+ */
212
+ function extractSourcePath(expr: ts.Expression): string | null {
213
+ if (ts.isIdentifier(expr)) {
214
+ return expr.text
215
+ }
216
+
217
+ if (ts.isPropertyAccessExpression(expr)) {
218
+ const base = extractSourcePath(expr.expression)
219
+ if (base) {
220
+ return `${base}.${expr.name.text}`
221
+ }
222
+ }
223
+
224
+ return null
225
+ }
226
+
121
227
  /**
122
228
  * Extract the variable name from a for..of statement
123
229
  */
@@ -135,17 +241,8 @@ export function extractForOfVariable(
135
241
 
136
242
  const itemVar = decl.name.text
137
243
 
138
- // Extract source variable
139
- let sourceVar: string | null = null
140
- if (ts.isIdentifier(node.expression)) {
141
- sourceVar = node.expression.text
142
- } else if (
143
- ts.isPropertyAccessExpression(node.expression) &&
144
- ts.isIdentifier(node.expression.expression)
145
- ) {
146
- // Handle data.memberEmails
147
- sourceVar = node.expression.expression.text
148
- }
244
+ // Extract source variable with full path (e.g., data.memberEmails)
245
+ const sourceVar = extractSourcePath(node.expression)
149
246
 
150
247
  if (!sourceVar) {
151
248
  return null
@@ -16,8 +16,10 @@ export interface ValidationError {
16
16
  * - VariableStatement (const/let declarations)
17
17
  * - ExpressionStatement (await workflow.do, await workflow.sleep, await Promise.all)
18
18
  * - IfStatement (branches)
19
+ * - SwitchStatement (switch/case)
19
20
  * - ForOfStatement (sequential fanout)
20
21
  * - ReturnStatement
22
+ * - ThrowStatement (for WorkflowCancelledException)
21
23
  * - Block (containers)
22
24
  */
23
25
  export function validateNoDisallowedPatterns(node: ts.Node): ValidationError[] {
@@ -29,8 +31,10 @@ export function validateNoDisallowedPatterns(node: ts.Node): ValidationError[] {
29
31
  ts.isVariableStatement(statement) ||
30
32
  ts.isExpressionStatement(statement) ||
31
33
  ts.isIfStatement(statement) ||
34
+ ts.isSwitchStatement(statement) ||
32
35
  ts.isForOfStatement(statement) ||
33
- ts.isReturnStatement(statement)
36
+ ts.isReturnStatement(statement) ||
37
+ ts.isThrowStatement(statement)
34
38
  ) {
35
39
  // Allowed statement type - recurse into it
36
40
  visitNode(statement)
@@ -38,7 +42,7 @@ export function validateNoDisallowedPatterns(node: ts.Node): ValidationError[] {
38
42
  // Unknown/disallowed statement type
39
43
  const nodeType = ts.SyntaxKind[statement.kind]
40
44
  errors.push({
41
- message: `Statement type '${nodeType}' is not allowed in simple workflows. Allowed: const/let, if/else, for..of, return, and workflow calls. If this should be supported, please report the node type: ${nodeType}`,
45
+ message: `Statement type '${nodeType}' is not allowed in simple workflows. Allowed: const/let, if/else, switch/case, for..of, return, throw, and workflow calls. If this should be supported, please report the node type: ${nodeType}`,
42
46
  node: statement,
43
47
  })
44
48
  }
@@ -109,7 +113,30 @@ export function validateNoDisallowedPatterns(node: ts.Node): ValidationError[] {
109
113
  export function validateAwaitedCalls(node: ts.Node): ValidationError[] {
110
114
  const errors: ValidationError[] = []
111
115
 
112
- function visit(node: ts.Node, parentIsAwait: boolean = false) {
116
+ function visit(
117
+ node: ts.Node,
118
+ parentIsAwait: boolean = false,
119
+ insidePromiseAll: boolean = false
120
+ ) {
121
+ // Check if this is Promise.all(...) first, before checking for workflow calls
122
+ if (
123
+ ts.isCallExpression(node) &&
124
+ ts.isPropertyAccessExpression(node.expression)
125
+ ) {
126
+ const propAccess = node.expression
127
+ if (
128
+ propAccess.name.text === 'all' &&
129
+ ts.isIdentifier(propAccess.expression) &&
130
+ propAccess.expression.text === 'Promise'
131
+ ) {
132
+ // console.log('[DEBUG] Found Promise.all, setting insidePromiseAll=true')
133
+ // Visit children with insidePromiseAll = true
134
+ ts.forEachChild(node, (child) => visit(child, parentIsAwait, true))
135
+ return
136
+ }
137
+ }
138
+
139
+ // Now check for workflow calls
113
140
  if (ts.isCallExpression(node)) {
114
141
  if (ts.isPropertyAccessExpression(node.expression)) {
115
142
  const propAccess = node.expression
@@ -118,7 +145,7 @@ export function validateAwaitedCalls(node: ts.Node): ValidationError[] {
118
145
  ts.isIdentifier(propAccess.expression) &&
119
146
  propAccess.expression.text === 'workflow'
120
147
  ) {
121
- if (!parentIsAwait) {
148
+ if (!parentIsAwait && !insidePromiseAll) {
122
149
  errors.push({
123
150
  message: `workflow.${propAccess.name.text}() must be awaited`,
124
151
  node,
@@ -130,10 +157,10 @@ export function validateAwaitedCalls(node: ts.Node): ValidationError[] {
130
157
  }
131
158
 
132
159
  if (ts.isAwaitExpression(node)) {
133
- // Mark child as awaited
134
- ts.forEachChild(node.expression, (child) => visit(child, true))
160
+ // Visit the expression itself with parentIsAwait=true
161
+ visit(node.expression, true, insidePromiseAll)
135
162
  } else {
136
- ts.forEachChild(node, (child) => visit(child, false))
163
+ ts.forEachChild(node, (child) => visit(child, false, insidePromiseAll))
137
164
  }
138
165
  }
139
166
 
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Converts DSL (Domain Specific Language) step-based format to graph node format
3
+ */
4
+ import type { WorkflowStepMeta, WorkflowsMeta } from '@pikku/core/workflow'
5
+ import type {
6
+ SerializedGraphNode,
7
+ SerializedWorkflowGraph,
8
+ FunctionNode,
9
+ FlowNode,
10
+ DataRef,
11
+ } from './workflow-graph.types.js'
12
+
13
+ function makeNodeId(
14
+ step: WorkflowStepMeta,
15
+ index: number,
16
+ prefix: string
17
+ ): string {
18
+ if ('stepName' in step && step.stepName) {
19
+ return step.stepName
20
+ }
21
+ return `${prefix}_${index}`
22
+ }
23
+
24
+ /**
25
+ * Check if a node is a terminal flow (no next step should follow)
26
+ */
27
+ function isTerminalFlow(node: SerializedGraphNode): boolean {
28
+ if ('flow' in node) {
29
+ // Cancel and return are terminal flows - they end execution
30
+ return node.flow === 'cancel' || node.flow === 'return'
31
+ }
32
+ return false
33
+ }
34
+
35
+ /**
36
+ * Convert InputSource to DataRef
37
+ */
38
+ function convertInputSource(source: {
39
+ from: string
40
+ path?: string
41
+ name?: string
42
+ value?: unknown
43
+ parts?: string[]
44
+ expressions?: unknown[]
45
+ }): unknown | DataRef {
46
+ if (source.from === 'literal') {
47
+ return source.value
48
+ }
49
+ if (source.from === 'input') {
50
+ return { $ref: 'trigger', path: source.path }
51
+ }
52
+ if (source.from === 'outputVar') {
53
+ return { $ref: source.name!, path: source.path }
54
+ }
55
+ if (source.from === 'item') {
56
+ return { $ref: '$item', path: source.path }
57
+ }
58
+ if (source.from === 'stateVar') {
59
+ return { $state: source.name!, path: source.path }
60
+ }
61
+ if (source.from === 'template') {
62
+ return {
63
+ $template: {
64
+ parts: source.parts,
65
+ expressions: source.expressions?.map((expr) =>
66
+ convertInputSource(expr as any)
67
+ ),
68
+ },
69
+ }
70
+ }
71
+ return source.value
72
+ }
73
+
74
+ /**
75
+ * Convert a single DSL step to graph node(s)
76
+ */
77
+ function convertStepToNode(
78
+ step: WorkflowStepMeta,
79
+ index: number,
80
+ steps: WorkflowStepMeta[],
81
+ nodeIdPrefix: string = 'step'
82
+ ): SerializedGraphNode[] {
83
+ const nodeId = makeNodeId(step, index, nodeIdPrefix)
84
+ const nextNodeId =
85
+ index < steps.length - 1
86
+ ? makeNodeId(steps[index + 1], index + 1, nodeIdPrefix)
87
+ : undefined
88
+
89
+ switch (step.type) {
90
+ case 'rpc': {
91
+ const node: FunctionNode = {
92
+ nodeId,
93
+ rpcName: step.rpcName,
94
+ next: nextNodeId,
95
+ }
96
+ if (step.inputs) {
97
+ if (step.inputs === 'passthrough') {
98
+ // Entire data is passed through - store as reference to trigger
99
+ node.input = { $passthrough: { $ref: 'trigger' } }
100
+ } else {
101
+ node.input = {}
102
+ for (const [key, source] of Object.entries(step.inputs)) {
103
+ node.input[key] = convertInputSource(source as any)
104
+ }
105
+ }
106
+ }
107
+ if (step.outputVar) {
108
+ node.outputVar = step.outputVar
109
+ }
110
+ if (step.options) {
111
+ node.options = {
112
+ retries: step.options.retries,
113
+ retryDelay: step.options.retryDelay?.toString(),
114
+ }
115
+ }
116
+ return [node]
117
+ }
118
+
119
+ case 'sleep': {
120
+ const node: FlowNode = {
121
+ nodeId,
122
+ flow: 'sleep',
123
+ duration: step.duration,
124
+ next: nextNodeId,
125
+ }
126
+ return [node]
127
+ }
128
+
129
+ case 'inline': {
130
+ const node: FlowNode = {
131
+ nodeId,
132
+ flow: 'inline',
133
+ description: step.description,
134
+ next: nextNodeId,
135
+ }
136
+ return [node]
137
+ }
138
+
139
+ case 'branch': {
140
+ // Convert all branch conditions (if/else-if chain)
141
+ const branchNodes: SerializedGraphNode[] = []
142
+ const branches: Array<{
143
+ condition: unknown
144
+ entry: string
145
+ }> = []
146
+
147
+ for (let i = 0; i < step.branches.length; i++) {
148
+ const branchSteps = convertStepsToNodes(
149
+ step.branches[i].steps,
150
+ `${nodeId}_branch${i}`
151
+ )
152
+ if (branchSteps.length > 0) {
153
+ branches.push({
154
+ condition: step.branches[i].condition,
155
+ entry: branchSteps[0].nodeId,
156
+ })
157
+ // Link last branch node back to next (unless terminal flow)
158
+ if (nextNodeId) {
159
+ const lastBranch = branchSteps[branchSteps.length - 1]
160
+ if (!lastBranch.next && !isTerminalFlow(lastBranch))
161
+ lastBranch.next = nextNodeId
162
+ }
163
+ branchNodes.push(...branchSteps)
164
+ }
165
+ }
166
+
167
+ // Convert else branch
168
+ const elseNodes = step.elseSteps
169
+ ? convertStepsToNodes(step.elseSteps, `${nodeId}_else`)
170
+ : []
171
+ if (elseNodes.length > 0 && nextNodeId) {
172
+ const lastElse = elseNodes[elseNodes.length - 1]
173
+ if (!lastElse.next && !isTerminalFlow(lastElse))
174
+ lastElse.next = nextNodeId
175
+ }
176
+
177
+ const node: FlowNode = {
178
+ nodeId,
179
+ flow: 'branch',
180
+ branches,
181
+ elseEntry: elseNodes.length > 0 ? elseNodes[0].nodeId : undefined,
182
+ next: nextNodeId,
183
+ }
184
+
185
+ return [node, ...branchNodes, ...elseNodes]
186
+ }
187
+
188
+ case 'switch': {
189
+ const caseNodes: SerializedGraphNode[] = []
190
+ const cases: Array<{
191
+ value?: unknown
192
+ expression?: string
193
+ entry: string
194
+ }> = []
195
+
196
+ for (let i = 0; i < step.cases.length; i++) {
197
+ const caseSteps = convertStepsToNodes(
198
+ step.cases[i].steps,
199
+ `${nodeId}_case${i}`
200
+ )
201
+ if (caseSteps.length > 0) {
202
+ cases.push({
203
+ value: step.cases[i].value,
204
+ expression: step.cases[i].expression,
205
+ entry: caseSteps[0].nodeId,
206
+ })
207
+ // Link last case node to next (unless terminal flow)
208
+ if (nextNodeId) {
209
+ const lastCase = caseSteps[caseSteps.length - 1]
210
+ if (!lastCase.next && !isTerminalFlow(lastCase))
211
+ lastCase.next = nextNodeId
212
+ }
213
+ caseNodes.push(...caseSteps)
214
+ }
215
+ }
216
+
217
+ let defaultEntry: string | undefined
218
+ if (step.defaultSteps) {
219
+ const defaultNodes = convertStepsToNodes(
220
+ step.defaultSteps,
221
+ `${nodeId}_default`
222
+ )
223
+ if (defaultNodes.length > 0) {
224
+ defaultEntry = defaultNodes[0].nodeId
225
+ // Link last default node to next (unless terminal flow)
226
+ if (nextNodeId) {
227
+ const lastDefault = defaultNodes[defaultNodes.length - 1]
228
+ if (!lastDefault.next && !isTerminalFlow(lastDefault))
229
+ lastDefault.next = nextNodeId
230
+ }
231
+ caseNodes.push(...defaultNodes)
232
+ }
233
+ }
234
+
235
+ const node: FlowNode = {
236
+ nodeId,
237
+ flow: 'switch',
238
+ expression: step.expression,
239
+ cases,
240
+ defaultEntry,
241
+ next: nextNodeId,
242
+ }
243
+
244
+ return [node, ...caseNodes]
245
+ }
246
+
247
+ case 'parallel': {
248
+ // Convert children to nodes
249
+ const childNodes: SerializedGraphNode[] = []
250
+ const childEntries: string[] = []
251
+
252
+ for (let i = 0; i < step.children.length; i++) {
253
+ const childSteps = convertStepToNode(
254
+ step.children[i],
255
+ i,
256
+ step.children,
257
+ `${nodeId}_child`
258
+ )
259
+ if (childSteps.length > 0) {
260
+ childEntries.push(childSteps[0].nodeId)
261
+ childNodes.push(...childSteps)
262
+ }
263
+ }
264
+
265
+ const node: FlowNode = {
266
+ nodeId,
267
+ flow: 'parallel',
268
+ children: childEntries,
269
+ next: nextNodeId,
270
+ }
271
+
272
+ return [node, ...childNodes]
273
+ }
274
+
275
+ case 'fanout': {
276
+ // Convert child step
277
+ const childNodes = convertStepToNode(
278
+ step.child,
279
+ 0,
280
+ [step.child],
281
+ `${nodeId}_item`
282
+ )
283
+
284
+ const node: FlowNode = {
285
+ nodeId,
286
+ flow: 'fanout',
287
+ sourceVar: step.sourceVar,
288
+ itemVar: step.itemVar,
289
+ mode: step.mode,
290
+ childEntry: childNodes.length > 0 ? childNodes[0].nodeId : undefined,
291
+ timeBetween: step.timeBetween,
292
+ next: nextNodeId,
293
+ }
294
+
295
+ return [node, ...childNodes]
296
+ }
297
+
298
+ case 'filter': {
299
+ const node: FlowNode = {
300
+ nodeId,
301
+ flow: 'filter',
302
+ sourceVar: step.sourceVar,
303
+ itemVar: step.itemVar,
304
+ condition: step.condition,
305
+ outputVar: step.outputVar,
306
+ next: nextNodeId,
307
+ }
308
+ return [node]
309
+ }
310
+
311
+ case 'arrayPredicate': {
312
+ const node: FlowNode = {
313
+ nodeId,
314
+ flow: 'arrayPredicate',
315
+ mode: step.mode,
316
+ sourceVar: step.sourceVar,
317
+ itemVar: step.itemVar,
318
+ condition: step.condition,
319
+ outputVar: step.outputVar,
320
+ next: nextNodeId,
321
+ }
322
+ return [node]
323
+ }
324
+
325
+ case 'return': {
326
+ const node: FlowNode = {
327
+ nodeId,
328
+ flow: 'return',
329
+ outputs: step.outputs,
330
+ }
331
+ return [node]
332
+ }
333
+
334
+ case 'cancel': {
335
+ const node: FlowNode = {
336
+ nodeId,
337
+ flow: 'cancel',
338
+ reason: step.reason,
339
+ }
340
+ return [node]
341
+ }
342
+
343
+ case 'set': {
344
+ const node: FlowNode = {
345
+ nodeId,
346
+ flow: 'set',
347
+ variable: step.variable,
348
+ value: step.value,
349
+ next: nextNodeId,
350
+ }
351
+ return [node]
352
+ }
353
+
354
+ default:
355
+ return []
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Convert array of steps to graph nodes
361
+ */
362
+ function convertStepsToNodes(
363
+ steps: WorkflowStepMeta[],
364
+ nodeIdPrefix: string = 'step'
365
+ ): SerializedGraphNode[] {
366
+ const allNodes: SerializedGraphNode[] = []
367
+
368
+ for (let i = 0; i < steps.length; i++) {
369
+ const nodes = convertStepToNode(steps[i], i, steps, nodeIdPrefix)
370
+ allNodes.push(...nodes)
371
+ }
372
+
373
+ return allNodes
374
+ }
375
+
376
+ /**
377
+ * Convert a DSL workflow to graph format
378
+ */
379
+ export function convertDslToGraph(
380
+ workflowName: string,
381
+ meta: WorkflowsMeta[string]
382
+ ): SerializedWorkflowGraph {
383
+ const nodes = convertStepsToNodes(meta.steps)
384
+ const nodesRecord: Record<string, SerializedGraphNode> = {}
385
+
386
+ for (const node of nodes) {
387
+ nodesRecord[node.nodeId] = node
388
+ }
389
+
390
+ const entryNodeIds = nodes.length > 0 ? [nodes[0].nodeId] : []
391
+
392
+ // Determine source type based on dsl flag:
393
+ // - dsl === true: pure DSL workflow, can be serialized
394
+ // - dsl === false: complex workflow with inline steps, not serializable
395
+ const source = meta.dsl === false ? 'complex' : 'dsl'
396
+
397
+ return {
398
+ name: workflowName,
399
+ pikkuFuncId: meta.pikkuFuncId,
400
+ source,
401
+ description: meta.description,
402
+ tags: meta.tags,
403
+ context: meta.context,
404
+ nodes: nodesRecord,
405
+ entryNodeIds,
406
+ }
407
+ }
408
+
409
+ /**
410
+ * Convert all DSL workflows to graph format
411
+ */
412
+ export function convertAllDslToGraphs(
413
+ workflowsMeta: WorkflowsMeta
414
+ ): Record<string, SerializedWorkflowGraph> {
415
+ const result: Record<string, SerializedWorkflowGraph> = {}
416
+
417
+ for (const [name, meta] of Object.entries(workflowsMeta)) {
418
+ result[name] = convertDslToGraph(name, meta)
419
+ }
420
+
421
+ return result
422
+ }