@pikku/inspector 0.11.0 → 0.11.2

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 (109) hide show
  1. package/CHANGELOG.md +32 -2
  2. package/dist/add/add-channel.js +11 -10
  3. package/dist/add/add-file-with-factory.js +10 -10
  4. package/dist/add/add-forge-credential.d.ts +8 -0
  5. package/dist/add/add-forge-credential.js +77 -0
  6. package/dist/add/add-forge-node.d.ts +7 -0
  7. package/dist/add/add-forge-node.js +77 -0
  8. package/dist/add/add-functions.js +158 -51
  9. package/dist/add/add-http-route.js +28 -4
  10. package/dist/add/add-mcp-prompt.js +6 -5
  11. package/dist/add/add-mcp-resource.js +6 -5
  12. package/dist/add/add-mcp-tool.js +6 -5
  13. package/dist/add/add-middleware.js +1 -1
  14. package/dist/add/add-permission.js +1 -1
  15. package/dist/add/add-queue-worker.js +6 -5
  16. package/dist/add/add-rpc-invocations.d.ts +3 -0
  17. package/dist/add/add-rpc-invocations.js +51 -25
  18. package/dist/add/add-schedule.js +5 -4
  19. package/dist/add/add-workflow-graph.d.ts +6 -0
  20. package/dist/add/add-workflow-graph.js +659 -0
  21. package/dist/add/add-workflow.d.ts +1 -1
  22. package/dist/add/add-workflow.js +191 -69
  23. package/dist/error-codes.d.ts +3 -0
  24. package/dist/error-codes.js +3 -0
  25. package/dist/index.d.ts +5 -0
  26. package/dist/index.js +3 -0
  27. package/dist/inspector.js +29 -9
  28. package/dist/types.d.ts +47 -8
  29. package/dist/utils/extract-function-name.js +7 -7
  30. package/dist/utils/extract-function-node.d.ts +10 -0
  31. package/dist/utils/extract-function-node.js +38 -0
  32. package/dist/utils/extract-node-value.d.ts +8 -0
  33. package/dist/utils/extract-node-value.js +24 -0
  34. package/dist/utils/extract-service-metadata.d.ts +19 -0
  35. package/dist/utils/extract-service-metadata.js +244 -0
  36. package/dist/utils/get-files-and-methods.d.ts +3 -3
  37. package/dist/utils/get-files-and-methods.js +3 -3
  38. package/dist/utils/get-property-value.d.ts +14 -6
  39. package/dist/utils/get-property-value.js +55 -43
  40. package/dist/utils/post-process.d.ts +9 -0
  41. package/dist/utils/post-process.js +30 -3
  42. package/dist/utils/serialize-inspector-state.d.ts +42 -6
  43. package/dist/utils/serialize-inspector-state.js +36 -10
  44. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.d.ts +24 -0
  45. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +898 -0
  46. package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +17 -0
  47. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +1284 -0
  48. package/dist/utils/workflow/dsl/index.d.ts +7 -0
  49. package/dist/utils/workflow/dsl/index.js +7 -0
  50. package/dist/utils/workflow/dsl/patterns.d.ts +60 -0
  51. package/dist/utils/workflow/dsl/patterns.js +218 -0
  52. package/dist/utils/workflow/dsl/validation.d.ts +30 -0
  53. package/dist/utils/workflow/dsl/validation.js +142 -0
  54. package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +13 -0
  55. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +316 -0
  56. package/dist/utils/workflow/graph/index.d.ts +6 -0
  57. package/dist/utils/workflow/graph/index.js +6 -0
  58. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +43 -0
  59. package/dist/utils/workflow/graph/serialize-workflow-graph.js +152 -0
  60. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +229 -0
  61. package/dist/utils/workflow/graph/workflow-graph.types.js +38 -0
  62. package/dist/utils/write-service-metadata.d.ts +13 -0
  63. package/dist/utils/write-service-metadata.js +37 -0
  64. package/dist/visit.js +8 -2
  65. package/package.json +16 -4
  66. package/src/add/add-channel.ts +37 -17
  67. package/src/add/add-file-with-factory.ts +10 -10
  68. package/src/add/add-forge-credential.ts +119 -0
  69. package/src/add/add-forge-node.ts +132 -0
  70. package/src/add/add-functions.ts +199 -69
  71. package/src/add/add-http-route.ts +34 -5
  72. package/src/add/add-mcp-prompt.ts +11 -7
  73. package/src/add/add-mcp-resource.ts +11 -7
  74. package/src/add/add-mcp-tool.ts +11 -7
  75. package/src/add/add-middleware.ts +1 -1
  76. package/src/add/add-permission.ts +1 -1
  77. package/src/add/add-queue-worker.ts +11 -12
  78. package/src/add/add-rpc-invocations.ts +61 -31
  79. package/src/add/add-schedule.ts +10 -5
  80. package/src/add/add-workflow-graph.ts +864 -0
  81. package/src/add/add-workflow.ts +212 -116
  82. package/src/error-codes.ts +3 -0
  83. package/src/index.ts +12 -0
  84. package/src/inspector.ts +36 -10
  85. package/src/types.ts +43 -9
  86. package/src/utils/extract-function-name.ts +7 -7
  87. package/src/utils/extract-function-node.ts +58 -0
  88. package/src/utils/extract-node-value.ts +31 -0
  89. package/src/utils/extract-service-metadata.ts +353 -0
  90. package/src/utils/filter-inspector-state.test.ts +3 -3
  91. package/src/utils/filter-utils.test.ts +45 -51
  92. package/src/utils/get-files-and-methods.ts +11 -11
  93. package/src/utils/get-property-value.ts +67 -53
  94. package/src/utils/permissions.test.ts +3 -3
  95. package/src/utils/post-process.ts +56 -3
  96. package/src/utils/serialize-inspector-state.ts +67 -19
  97. package/src/utils/test-data/inspector-state.json +9 -9
  98. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +1180 -0
  99. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +1608 -0
  100. package/src/utils/workflow/dsl/index.ts +11 -0
  101. package/src/utils/workflow/dsl/patterns.ts +279 -0
  102. package/src/utils/workflow/dsl/validation.ts +180 -0
  103. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +415 -0
  104. package/src/utils/workflow/graph/index.ts +6 -0
  105. package/src/utils/workflow/graph/serialize-workflow-graph.ts +223 -0
  106. package/src/utils/workflow/graph/workflow-graph.types.ts +280 -0
  107. package/src/utils/write-service-metadata.ts +51 -0
  108. package/src/visit.ts +9 -3
  109. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,415 @@
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
+ /**
14
+ * Check if a node is a terminal flow (no next step should follow)
15
+ */
16
+ function isTerminalFlow(node: SerializedGraphNode): boolean {
17
+ if ('flow' in node) {
18
+ // Cancel and return are terminal flows - they end execution
19
+ return node.flow === 'cancel' || node.flow === 'return'
20
+ }
21
+ return false
22
+ }
23
+
24
+ /**
25
+ * Convert InputSource to DataRef
26
+ */
27
+ function convertInputSource(source: {
28
+ from: string
29
+ path?: string
30
+ name?: string
31
+ value?: unknown
32
+ parts?: string[]
33
+ expressions?: unknown[]
34
+ }): unknown | DataRef {
35
+ if (source.from === 'literal') {
36
+ return source.value
37
+ }
38
+ if (source.from === 'input') {
39
+ return { $ref: 'trigger', path: source.path }
40
+ }
41
+ if (source.from === 'outputVar') {
42
+ return { $ref: source.name!, path: source.path }
43
+ }
44
+ if (source.from === 'item') {
45
+ return { $ref: '$item', path: source.path }
46
+ }
47
+ if (source.from === 'stateVar') {
48
+ return { $state: source.name!, path: source.path }
49
+ }
50
+ if (source.from === 'template') {
51
+ return {
52
+ $template: {
53
+ parts: source.parts,
54
+ expressions: source.expressions?.map((expr) =>
55
+ convertInputSource(expr as any)
56
+ ),
57
+ },
58
+ }
59
+ }
60
+ return source.value
61
+ }
62
+
63
+ /**
64
+ * Convert a single DSL step to graph node(s)
65
+ */
66
+ function convertStepToNode(
67
+ step: WorkflowStepMeta,
68
+ index: number,
69
+ steps: WorkflowStepMeta[],
70
+ nodeIdPrefix: string = 'step'
71
+ ): SerializedGraphNode[] {
72
+ const nodeId = `${nodeIdPrefix}_${index}`
73
+ const nextNodeId =
74
+ index < steps.length - 1 ? `${nodeIdPrefix}_${index + 1}` : undefined
75
+
76
+ switch (step.type) {
77
+ case 'rpc': {
78
+ const node: FunctionNode = {
79
+ nodeId,
80
+ rpcName: step.rpcName,
81
+ stepName: step.stepName,
82
+ next: nextNodeId,
83
+ }
84
+ if (step.inputs) {
85
+ if (step.inputs === 'passthrough') {
86
+ // Entire data is passed through - store as reference to trigger
87
+ node.input = { $passthrough: { $ref: 'trigger' } }
88
+ } else {
89
+ node.input = {}
90
+ for (const [key, source] of Object.entries(step.inputs)) {
91
+ node.input[key] = convertInputSource(source as any)
92
+ }
93
+ }
94
+ }
95
+ if (step.outputVar) {
96
+ node.outputVar = step.outputVar
97
+ }
98
+ if (step.options) {
99
+ node.options = {
100
+ retries: step.options.retries,
101
+ retryDelay: step.options.retryDelay?.toString(),
102
+ }
103
+ }
104
+ return [node]
105
+ }
106
+
107
+ case 'sleep': {
108
+ const node: FlowNode = {
109
+ nodeId,
110
+ flow: 'sleep',
111
+ stepName: step.stepName,
112
+ duration: step.duration,
113
+ next: nextNodeId,
114
+ }
115
+ return [node]
116
+ }
117
+
118
+ case 'inline': {
119
+ const node: FlowNode = {
120
+ nodeId,
121
+ flow: 'inline',
122
+ stepName: step.stepName,
123
+ description: step.description,
124
+ next: nextNodeId,
125
+ }
126
+ return [node]
127
+ }
128
+
129
+ case 'branch': {
130
+ // Convert all branch conditions (if/else-if chain)
131
+ const branchNodes: SerializedGraphNode[] = []
132
+ const branches: Array<{
133
+ condition: unknown
134
+ entry: string
135
+ }> = []
136
+
137
+ for (let i = 0; i < step.branches.length; i++) {
138
+ const branchSteps = convertStepsToNodes(
139
+ step.branches[i].steps,
140
+ `${nodeId}_branch${i}`
141
+ )
142
+ if (branchSteps.length > 0) {
143
+ branches.push({
144
+ condition: step.branches[i].condition,
145
+ entry: branchSteps[0].nodeId,
146
+ })
147
+ // Link last branch node back to next (unless terminal flow)
148
+ if (nextNodeId) {
149
+ const lastBranch = branchSteps[branchSteps.length - 1]
150
+ if (!lastBranch.next && !isTerminalFlow(lastBranch))
151
+ lastBranch.next = nextNodeId
152
+ }
153
+ branchNodes.push(...branchSteps)
154
+ }
155
+ }
156
+
157
+ // Convert else branch
158
+ const elseNodes = step.elseSteps
159
+ ? convertStepsToNodes(step.elseSteps, `${nodeId}_else`)
160
+ : []
161
+ if (elseNodes.length > 0 && nextNodeId) {
162
+ const lastElse = elseNodes[elseNodes.length - 1]
163
+ if (!lastElse.next && !isTerminalFlow(lastElse))
164
+ lastElse.next = nextNodeId
165
+ }
166
+
167
+ const node: FlowNode = {
168
+ nodeId,
169
+ flow: 'branch',
170
+ branches,
171
+ elseEntry: elseNodes.length > 0 ? elseNodes[0].nodeId : undefined,
172
+ next: nextNodeId,
173
+ }
174
+
175
+ return [node, ...branchNodes, ...elseNodes]
176
+ }
177
+
178
+ case 'switch': {
179
+ const caseNodes: SerializedGraphNode[] = []
180
+ const cases: Array<{
181
+ value?: unknown
182
+ expression?: string
183
+ entry: string
184
+ }> = []
185
+
186
+ for (let i = 0; i < step.cases.length; i++) {
187
+ const caseSteps = convertStepsToNodes(
188
+ step.cases[i].steps,
189
+ `${nodeId}_case${i}`
190
+ )
191
+ if (caseSteps.length > 0) {
192
+ cases.push({
193
+ value: step.cases[i].value,
194
+ expression: step.cases[i].expression,
195
+ entry: caseSteps[0].nodeId,
196
+ })
197
+ // Link last case node to next (unless terminal flow)
198
+ if (nextNodeId) {
199
+ const lastCase = caseSteps[caseSteps.length - 1]
200
+ if (!lastCase.next && !isTerminalFlow(lastCase))
201
+ lastCase.next = nextNodeId
202
+ }
203
+ caseNodes.push(...caseSteps)
204
+ }
205
+ }
206
+
207
+ let defaultEntry: string | undefined
208
+ if (step.defaultSteps) {
209
+ const defaultNodes = convertStepsToNodes(
210
+ step.defaultSteps,
211
+ `${nodeId}_default`
212
+ )
213
+ if (defaultNodes.length > 0) {
214
+ defaultEntry = defaultNodes[0].nodeId
215
+ // Link last default node to next (unless terminal flow)
216
+ if (nextNodeId) {
217
+ const lastDefault = defaultNodes[defaultNodes.length - 1]
218
+ if (!lastDefault.next && !isTerminalFlow(lastDefault))
219
+ lastDefault.next = nextNodeId
220
+ }
221
+ caseNodes.push(...defaultNodes)
222
+ }
223
+ }
224
+
225
+ const node: FlowNode = {
226
+ nodeId,
227
+ flow: 'switch',
228
+ expression: step.expression,
229
+ cases,
230
+ defaultEntry,
231
+ next: nextNodeId,
232
+ }
233
+
234
+ return [node, ...caseNodes]
235
+ }
236
+
237
+ case 'parallel': {
238
+ // Convert children to nodes
239
+ const childNodes: SerializedGraphNode[] = []
240
+ const childEntries: string[] = []
241
+
242
+ for (let i = 0; i < step.children.length; i++) {
243
+ const childSteps = convertStepToNode(
244
+ step.children[i],
245
+ i,
246
+ step.children,
247
+ `${nodeId}_child`
248
+ )
249
+ if (childSteps.length > 0) {
250
+ childEntries.push(childSteps[0].nodeId)
251
+ childNodes.push(...childSteps)
252
+ }
253
+ }
254
+
255
+ const node: FlowNode = {
256
+ nodeId,
257
+ flow: 'parallel',
258
+ children: childEntries,
259
+ next: nextNodeId,
260
+ }
261
+
262
+ return [node, ...childNodes]
263
+ }
264
+
265
+ case 'fanout': {
266
+ // Convert child step
267
+ const childNodes = convertStepToNode(
268
+ step.child,
269
+ 0,
270
+ [step.child],
271
+ `${nodeId}_item`
272
+ )
273
+
274
+ const node: FlowNode = {
275
+ nodeId,
276
+ flow: 'fanout',
277
+ stepName: step.stepName,
278
+ sourceVar: step.sourceVar,
279
+ itemVar: step.itemVar,
280
+ mode: step.mode,
281
+ childEntry: childNodes.length > 0 ? childNodes[0].nodeId : undefined,
282
+ timeBetween: step.timeBetween,
283
+ next: nextNodeId,
284
+ }
285
+
286
+ return [node, ...childNodes]
287
+ }
288
+
289
+ case 'filter': {
290
+ const node: FlowNode = {
291
+ nodeId,
292
+ flow: 'filter',
293
+ sourceVar: step.sourceVar,
294
+ itemVar: step.itemVar,
295
+ condition: step.condition,
296
+ outputVar: step.outputVar,
297
+ next: nextNodeId,
298
+ }
299
+ return [node]
300
+ }
301
+
302
+ case 'arrayPredicate': {
303
+ const node: FlowNode = {
304
+ nodeId,
305
+ flow: 'arrayPredicate',
306
+ mode: step.mode,
307
+ sourceVar: step.sourceVar,
308
+ itemVar: step.itemVar,
309
+ condition: step.condition,
310
+ outputVar: step.outputVar,
311
+ next: nextNodeId,
312
+ }
313
+ return [node]
314
+ }
315
+
316
+ case 'return': {
317
+ const node: FlowNode = {
318
+ nodeId,
319
+ flow: 'return',
320
+ outputs: step.outputs,
321
+ }
322
+ return [node]
323
+ }
324
+
325
+ case 'cancel': {
326
+ const node: FlowNode = {
327
+ nodeId,
328
+ flow: 'cancel',
329
+ reason: step.reason,
330
+ }
331
+ return [node]
332
+ }
333
+
334
+ case 'set': {
335
+ const node: FlowNode = {
336
+ nodeId,
337
+ flow: 'set',
338
+ variable: step.variable,
339
+ value: step.value,
340
+ next: nextNodeId,
341
+ }
342
+ return [node]
343
+ }
344
+
345
+ default:
346
+ return []
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Convert array of steps to graph nodes
352
+ */
353
+ function convertStepsToNodes(
354
+ steps: WorkflowStepMeta[],
355
+ nodeIdPrefix: string = 'step'
356
+ ): SerializedGraphNode[] {
357
+ const allNodes: SerializedGraphNode[] = []
358
+
359
+ for (let i = 0; i < steps.length; i++) {
360
+ const nodes = convertStepToNode(steps[i], i, steps, nodeIdPrefix)
361
+ allNodes.push(...nodes)
362
+ }
363
+
364
+ return allNodes
365
+ }
366
+
367
+ /**
368
+ * Convert a DSL workflow to graph format
369
+ */
370
+ export function convertDslToGraph(
371
+ workflowName: string,
372
+ meta: WorkflowsMeta[string]
373
+ ): SerializedWorkflowGraph {
374
+ const nodes = convertStepsToNodes(meta.steps)
375
+ const nodesRecord: Record<string, SerializedGraphNode> = {}
376
+
377
+ for (const node of nodes) {
378
+ nodesRecord[node.nodeId] = node
379
+ }
380
+
381
+ // Find entry nodes (step_0 is always entry for sequential workflows)
382
+ const entryNodeIds = nodes.length > 0 ? ['step_0'] : []
383
+
384
+ // Determine source type based on dsl flag:
385
+ // - dsl === true: pure DSL workflow, can be serialized
386
+ // - dsl === false: complex workflow with inline steps, not serializable
387
+ const source = meta.dsl === false ? 'complex' : 'dsl'
388
+
389
+ return {
390
+ name: workflowName,
391
+ pikkuFuncName: meta.pikkuFuncName,
392
+ source,
393
+ description: meta.description,
394
+ tags: meta.tags,
395
+ context: meta.context,
396
+ wires: {}, // DSL workflows don't have explicit wires in meta
397
+ nodes: nodesRecord,
398
+ entryNodeIds,
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Convert all DSL workflows to graph format
404
+ */
405
+ export function convertAllDslToGraphs(
406
+ workflowsMeta: WorkflowsMeta
407
+ ): Record<string, SerializedWorkflowGraph> {
408
+ const result: Record<string, SerializedWorkflowGraph> = {}
409
+
410
+ for (const [name, meta] of Object.entries(workflowsMeta)) {
411
+ result[name] = convertDslToGraph(name, meta)
412
+ }
413
+
414
+ return result
415
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Workflow graph serialization exports
3
+ */
4
+ export * from './workflow-graph.types.js'
5
+ export { serializeWorkflowGraph } from './serialize-workflow-graph.js'
6
+ export { convertDslToGraph } from './convert-dsl-to-graph.js'
@@ -0,0 +1,223 @@
1
+ import type {
2
+ SerializedWorkflowGraph,
3
+ SerializedGraphNode,
4
+ FunctionNode,
5
+ DataRef,
6
+ SerializedNext,
7
+ } from './workflow-graph.types.js'
8
+
9
+ /**
10
+ * Convert a RefValue (from runtime) to DataRef (serialized)
11
+ */
12
+ function convertRef(ref: { nodeId: string; path?: string }): DataRef {
13
+ return {
14
+ $ref: ref.nodeId,
15
+ path: ref.path,
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Check if a value is a runtime RefValue
21
+ */
22
+ function isRefValue(
23
+ value: unknown
24
+ ): value is { __isRef: true; nodeId: string; path?: string } {
25
+ return (
26
+ typeof value === 'object' &&
27
+ value !== null &&
28
+ '__isRef' in value &&
29
+ (value as any).__isRef === true
30
+ )
31
+ }
32
+
33
+ /**
34
+ * Convert input mapping from runtime format to serialized format
35
+ */
36
+ function serializeInputMapping(
37
+ input: Record<string, unknown>
38
+ ): Record<string, unknown | DataRef> {
39
+ const result: Record<string, unknown | DataRef> = {}
40
+
41
+ for (const [key, value] of Object.entries(input)) {
42
+ if (isRefValue(value)) {
43
+ result[key] = convertRef(value)
44
+ } else {
45
+ result[key] = value
46
+ }
47
+ }
48
+
49
+ return result
50
+ }
51
+
52
+ /**
53
+ * Convert next config from runtime format to serialized format
54
+ * Runtime uses Record<string, string | string[]> for branching with graph.branch()
55
+ * Serialized uses { conditions: [...], default: ... } for UI-friendly branching
56
+ */
57
+ function serializeNext(
58
+ next: string | string[] | Record<string, string | string[]> | undefined
59
+ ): SerializedNext | undefined {
60
+ if (!next) return undefined
61
+
62
+ if (typeof next === 'string') return next
63
+ if (Array.isArray(next)) return next
64
+
65
+ // Record format - convert to conditions format
66
+ // For now, treat keys as branch identifiers (from graph.branch())
67
+ // UI can display these as condition labels
68
+ const conditions = Object.entries(next).map(([key, target]) => ({
69
+ expression: key, // The branch key becomes the expression
70
+ target,
71
+ }))
72
+
73
+ return { conditions }
74
+ }
75
+
76
+ /**
77
+ * Serialize a workflow graph definition (from runtime) to JSON format
78
+ *
79
+ * @param definition - The runtime definition (with callbacks evaluated)
80
+ * @param rpcNameLookup - Function to get RPC name from a node's func
81
+ */
82
+ export function serializeWorkflowGraph(
83
+ definition: {
84
+ name: string
85
+ wires: {
86
+ http?: { route: string; method: string }
87
+ queue?: string
88
+ }
89
+ graph: Record<
90
+ string,
91
+ {
92
+ func: { name?: string }
93
+ input?: (ref: any) => Record<string, unknown>
94
+ next?: string | string[] | Record<string, string | string[]>
95
+ onError?: string | string[]
96
+ }
97
+ >
98
+ },
99
+ options?: {
100
+ description?: string
101
+ tags?: string[]
102
+ }
103
+ ): SerializedWorkflowGraph {
104
+ const nodes: Record<string, SerializedGraphNode> = {}
105
+ const entryNodeIds: string[] = []
106
+
107
+ // Create a ref function that captures refs
108
+ const createRef = (nodeId: string, path?: string) => ({
109
+ __isRef: true as const,
110
+ nodeId,
111
+ path,
112
+ })
113
+
114
+ // Track which nodes have incoming edges
115
+ const hasIncomingEdge = new Set<string>()
116
+
117
+ // First pass: identify nodes with incoming edges
118
+ for (const [_nodeId, node] of Object.entries(definition.graph)) {
119
+ const next = node.next
120
+ if (!next) continue
121
+
122
+ if (typeof next === 'string') {
123
+ hasIncomingEdge.add(next)
124
+ } else if (Array.isArray(next)) {
125
+ next.forEach((n) => hasIncomingEdge.add(n))
126
+ } else {
127
+ for (const targets of Object.values(next)) {
128
+ if (typeof targets === 'string') {
129
+ hasIncomingEdge.add(targets)
130
+ } else {
131
+ targets.forEach((n) => hasIncomingEdge.add(n))
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ // Second pass: serialize nodes
138
+ for (const [nodeId, node] of Object.entries(definition.graph)) {
139
+ // Evaluate input callback to get the mapping
140
+ let input: Record<string, unknown | DataRef> = {}
141
+ if (node.input) {
142
+ const rawInput = node.input(createRef)
143
+ input = serializeInputMapping(rawInput)
144
+ }
145
+
146
+ // Get RPC name from func
147
+ const rpcName = node.func?.name || 'unknown'
148
+
149
+ const funcNode: FunctionNode = {
150
+ nodeId,
151
+ rpcName,
152
+ input,
153
+ next: serializeNext(node.next),
154
+ onError: node.onError,
155
+ }
156
+ nodes[nodeId] = funcNode
157
+
158
+ // Entry nodes have no incoming edges
159
+ if (!hasIncomingEdge.has(nodeId)) {
160
+ entryNodeIds.push(nodeId)
161
+ }
162
+ }
163
+
164
+ return {
165
+ name: definition.name,
166
+ pikkuFuncName: definition.name, // For graph workflows, pikkuFuncName is the workflow name
167
+ source: 'graph' as const,
168
+ description: options?.description,
169
+ tags: options?.tags,
170
+ wires: definition.wires as SerializedWorkflowGraph['wires'],
171
+ nodes,
172
+ entryNodeIds,
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Deserialize a workflow graph from JSON to runtime format
178
+ * This re-hydrates the JSON so it can be executed
179
+ */
180
+ export function deserializeWorkflowGraph(serialized: SerializedWorkflowGraph): {
181
+ name: string
182
+ wires: SerializedWorkflowGraph['wires']
183
+ graph: Record<
184
+ string,
185
+ {
186
+ rpcName: string
187
+ input: Record<string, unknown | DataRef>
188
+ next?: SerializedNext
189
+ onError?: string | string[]
190
+ }
191
+ >
192
+ entryNodeIds: string[]
193
+ } {
194
+ const graph: Record<
195
+ string,
196
+ {
197
+ rpcName: string
198
+ input: Record<string, unknown | DataRef>
199
+ next?: SerializedNext
200
+ onError?: string | string[]
201
+ }
202
+ > = {}
203
+
204
+ for (const [nodeId, node] of Object.entries(serialized.nodes)) {
205
+ // Only include FunctionNode properties (nodes with rpcName)
206
+ if ('rpcName' in node) {
207
+ const funcNode = node as FunctionNode
208
+ graph[nodeId] = {
209
+ rpcName: funcNode.rpcName,
210
+ input: funcNode.input ?? {},
211
+ next: funcNode.next,
212
+ onError: funcNode.onError,
213
+ }
214
+ }
215
+ }
216
+
217
+ return {
218
+ name: serialized.name,
219
+ wires: serialized.wires,
220
+ graph,
221
+ entryNodeIds: serialized.entryNodeIds,
222
+ }
223
+ }