@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,864 @@
1
+ import * as ts from 'typescript'
2
+ import type { AddWiring } from '../types.js'
3
+ import { ErrorCode } from '../error-codes.js'
4
+ import { extractStringLiteral } from '../utils/extract-node-value.js'
5
+ import type {
6
+ SerializedWorkflowGraph,
7
+ DataRef,
8
+ WorkflowWiresConfig,
9
+ HttpWire,
10
+ ChannelWire,
11
+ QueueWire,
12
+ CliWire,
13
+ McpWires,
14
+ ScheduleWire,
15
+ TriggerWire,
16
+ } from '../utils/workflow/graph/workflow-graph.types.js'
17
+
18
+ /**
19
+ * Extract wire configuration from object literal
20
+ */
21
+ function extractWiresConfig(
22
+ wiresNode: ts.ObjectLiteralExpression,
23
+ checker: ts.TypeChecker
24
+ ): WorkflowWiresConfig {
25
+ const wires: WorkflowWiresConfig = {}
26
+
27
+ for (const prop of wiresNode.properties) {
28
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue
29
+
30
+ const propName = prop.name.text
31
+
32
+ if (propName === 'http' && ts.isArrayLiteralExpression(prop.initializer)) {
33
+ wires.http = []
34
+ for (const elem of prop.initializer.elements) {
35
+ if (ts.isObjectLiteralExpression(elem)) {
36
+ const httpWire: Partial<HttpWire> = {}
37
+ for (const httpProp of elem.properties) {
38
+ if (
39
+ !ts.isPropertyAssignment(httpProp) ||
40
+ !ts.isIdentifier(httpProp.name)
41
+ )
42
+ continue
43
+ const httpPropName = httpProp.name.text
44
+ if (httpPropName === 'route') {
45
+ httpWire.route = extractStringLiteral(
46
+ httpProp.initializer,
47
+ checker
48
+ )
49
+ } else if (httpPropName === 'method') {
50
+ httpWire.method = extractStringLiteral(
51
+ httpProp.initializer,
52
+ checker
53
+ ) as HttpWire['method']
54
+ } else if (httpPropName === 'startNode') {
55
+ httpWire.startNode = extractStringLiteral(
56
+ httpProp.initializer,
57
+ checker
58
+ )
59
+ }
60
+ }
61
+ if (httpWire.route && httpWire.method && httpWire.startNode) {
62
+ wires.http.push(httpWire as HttpWire)
63
+ }
64
+ }
65
+ }
66
+ } else if (
67
+ propName === 'channel' &&
68
+ ts.isArrayLiteralExpression(prop.initializer)
69
+ ) {
70
+ wires.channel = []
71
+ for (const elem of prop.initializer.elements) {
72
+ if (ts.isObjectLiteralExpression(elem)) {
73
+ const channelWire: Partial<ChannelWire> = {}
74
+ for (const channelProp of elem.properties) {
75
+ if (
76
+ !ts.isPropertyAssignment(channelProp) ||
77
+ !ts.isIdentifier(channelProp.name)
78
+ )
79
+ continue
80
+ const channelPropName = channelProp.name.text
81
+ if (channelPropName === 'name') {
82
+ channelWire.name = extractStringLiteral(
83
+ channelProp.initializer,
84
+ checker
85
+ )
86
+ } else if (channelPropName === 'onConnect') {
87
+ channelWire.onConnect = extractStringLiteral(
88
+ channelProp.initializer,
89
+ checker
90
+ )
91
+ } else if (channelPropName === 'onDisconnect') {
92
+ channelWire.onDisconnect = extractStringLiteral(
93
+ channelProp.initializer,
94
+ checker
95
+ )
96
+ } else if (channelPropName === 'onMessage') {
97
+ channelWire.onMessage = extractStringLiteral(
98
+ channelProp.initializer,
99
+ checker
100
+ )
101
+ }
102
+ }
103
+ if (channelWire.name) {
104
+ wires.channel.push(channelWire as ChannelWire)
105
+ }
106
+ }
107
+ }
108
+ } else if (
109
+ propName === 'queue' &&
110
+ ts.isArrayLiteralExpression(prop.initializer)
111
+ ) {
112
+ wires.queue = []
113
+ for (const elem of prop.initializer.elements) {
114
+ if (ts.isObjectLiteralExpression(elem)) {
115
+ const queueWire: Partial<QueueWire> = {}
116
+ for (const queueProp of elem.properties) {
117
+ if (
118
+ !ts.isPropertyAssignment(queueProp) ||
119
+ !ts.isIdentifier(queueProp.name)
120
+ )
121
+ continue
122
+ const queuePropName = queueProp.name.text
123
+ if (queuePropName === 'name') {
124
+ queueWire.name = extractStringLiteral(
125
+ queueProp.initializer,
126
+ checker
127
+ )
128
+ } else if (queuePropName === 'startNode') {
129
+ queueWire.startNode = extractStringLiteral(
130
+ queueProp.initializer,
131
+ checker
132
+ )
133
+ }
134
+ }
135
+ if (queueWire.name && queueWire.startNode) {
136
+ wires.queue.push(queueWire as QueueWire)
137
+ }
138
+ }
139
+ }
140
+ } else if (
141
+ propName === 'cli' &&
142
+ ts.isArrayLiteralExpression(prop.initializer)
143
+ ) {
144
+ wires.cli = []
145
+ for (const elem of prop.initializer.elements) {
146
+ if (ts.isObjectLiteralExpression(elem)) {
147
+ const cliWire: Partial<CliWire> = {}
148
+ for (const cliProp of elem.properties) {
149
+ if (
150
+ !ts.isPropertyAssignment(cliProp) ||
151
+ !ts.isIdentifier(cliProp.name)
152
+ )
153
+ continue
154
+ const cliPropName = cliProp.name.text
155
+ if (cliPropName === 'command') {
156
+ cliWire.command = extractStringLiteral(
157
+ cliProp.initializer,
158
+ checker
159
+ )
160
+ } else if (cliPropName === 'startNode') {
161
+ cliWire.startNode = extractStringLiteral(
162
+ cliProp.initializer,
163
+ checker
164
+ )
165
+ }
166
+ }
167
+ if (cliWire.command && cliWire.startNode) {
168
+ wires.cli.push(cliWire as CliWire)
169
+ }
170
+ }
171
+ }
172
+ } else if (
173
+ propName === 'mcp' &&
174
+ ts.isObjectLiteralExpression(prop.initializer)
175
+ ) {
176
+ const mcpWires: McpWires = {}
177
+ for (const mcpProp of prop.initializer.properties) {
178
+ if (!ts.isPropertyAssignment(mcpProp) || !ts.isIdentifier(mcpProp.name))
179
+ continue
180
+ const mcpPropName = mcpProp.name.text
181
+ if (
182
+ mcpPropName === 'tool' &&
183
+ ts.isArrayLiteralExpression(mcpProp.initializer)
184
+ ) {
185
+ mcpWires.tool = extractMcpToolWireArray(mcpProp.initializer, checker)
186
+ } else if (
187
+ mcpPropName === 'prompt' &&
188
+ ts.isArrayLiteralExpression(mcpProp.initializer)
189
+ ) {
190
+ mcpWires.prompt = extractMcpToolWireArray(
191
+ mcpProp.initializer,
192
+ checker
193
+ )
194
+ } else if (
195
+ mcpPropName === 'resource' &&
196
+ ts.isArrayLiteralExpression(mcpProp.initializer)
197
+ ) {
198
+ mcpWires.resource = extractMcpResourceWireArray(
199
+ mcpProp.initializer,
200
+ checker
201
+ )
202
+ }
203
+ }
204
+ if (mcpWires.tool || mcpWires.prompt || mcpWires.resource) {
205
+ wires.mcp = mcpWires
206
+ }
207
+ } else if (
208
+ propName === 'schedule' &&
209
+ ts.isArrayLiteralExpression(prop.initializer)
210
+ ) {
211
+ wires.schedule = []
212
+ for (const elem of prop.initializer.elements) {
213
+ if (ts.isObjectLiteralExpression(elem)) {
214
+ const scheduleWire: Partial<ScheduleWire> = {}
215
+ for (const scheduleProp of elem.properties) {
216
+ if (
217
+ !ts.isPropertyAssignment(scheduleProp) ||
218
+ !ts.isIdentifier(scheduleProp.name)
219
+ )
220
+ continue
221
+ const schedulePropName = scheduleProp.name.text
222
+ if (schedulePropName === 'cron') {
223
+ scheduleWire.cron = extractStringLiteral(
224
+ scheduleProp.initializer,
225
+ checker
226
+ )
227
+ } else if (schedulePropName === 'interval') {
228
+ scheduleWire.interval = extractStringLiteral(
229
+ scheduleProp.initializer,
230
+ checker
231
+ )
232
+ } else if (schedulePropName === 'startNode') {
233
+ scheduleWire.startNode = extractStringLiteral(
234
+ scheduleProp.initializer,
235
+ checker
236
+ )
237
+ }
238
+ }
239
+ if (
240
+ (scheduleWire.cron || scheduleWire.interval) &&
241
+ scheduleWire.startNode
242
+ ) {
243
+ wires.schedule.push(scheduleWire as ScheduleWire)
244
+ }
245
+ }
246
+ }
247
+ } else if (
248
+ propName === 'trigger' &&
249
+ ts.isArrayLiteralExpression(prop.initializer)
250
+ ) {
251
+ wires.trigger = []
252
+ for (const elem of prop.initializer.elements) {
253
+ if (ts.isObjectLiteralExpression(elem)) {
254
+ const triggerWire: Partial<TriggerWire> = {}
255
+ for (const triggerProp of elem.properties) {
256
+ if (
257
+ !ts.isPropertyAssignment(triggerProp) ||
258
+ !ts.isIdentifier(triggerProp.name)
259
+ )
260
+ continue
261
+ const triggerPropName = triggerProp.name.text
262
+ if (triggerPropName === 'name') {
263
+ triggerWire.name = extractStringLiteral(
264
+ triggerProp.initializer,
265
+ checker
266
+ )
267
+ } else if (triggerPropName === 'startNode') {
268
+ triggerWire.startNode = extractStringLiteral(
269
+ triggerProp.initializer,
270
+ checker
271
+ )
272
+ }
273
+ }
274
+ if (triggerWire.name && triggerWire.startNode) {
275
+ wires.trigger.push(triggerWire as TriggerWire)
276
+ }
277
+ }
278
+ }
279
+ }
280
+ }
281
+
282
+ return wires
283
+ }
284
+
285
+ /**
286
+ * Helper to extract MCP wire arrays for tool/prompt (name field)
287
+ */
288
+ function extractMcpToolWireArray(
289
+ arrayNode: ts.ArrayLiteralExpression,
290
+ checker: ts.TypeChecker
291
+ ): Array<{ name: string; startNode: string }> {
292
+ const result: Array<{ name: string; startNode: string }> = []
293
+ for (const elem of arrayNode.elements) {
294
+ if (ts.isObjectLiteralExpression(elem)) {
295
+ let name: string | undefined
296
+ let startNode: string | undefined
297
+ for (const prop of elem.properties) {
298
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
299
+ continue
300
+ const propName = prop.name.text
301
+ if (propName === 'name') {
302
+ name = extractStringLiteral(prop.initializer, checker)
303
+ } else if (propName === 'startNode') {
304
+ startNode = extractStringLiteral(prop.initializer, checker)
305
+ }
306
+ }
307
+ if (name && startNode) {
308
+ result.push({ name, startNode })
309
+ }
310
+ }
311
+ }
312
+ return result
313
+ }
314
+
315
+ /**
316
+ * Helper to extract MCP wire arrays for resource (uri field)
317
+ */
318
+ function extractMcpResourceWireArray(
319
+ arrayNode: ts.ArrayLiteralExpression,
320
+ checker: ts.TypeChecker
321
+ ): Array<{ uri: string; startNode: string }> {
322
+ const result: Array<{ uri: string; startNode: string }> = []
323
+ for (const elem of arrayNode.elements) {
324
+ if (ts.isObjectLiteralExpression(elem)) {
325
+ let uri: string | undefined
326
+ let startNode: string | undefined
327
+ for (const prop of elem.properties) {
328
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
329
+ continue
330
+ const propName = prop.name.text
331
+ if (propName === 'uri') {
332
+ uri = extractStringLiteral(prop.initializer, checker)
333
+ } else if (propName === 'startNode') {
334
+ startNode = extractStringLiteral(prop.initializer, checker)
335
+ }
336
+ }
337
+ if (uri && startNode) {
338
+ result.push({ uri, startNode })
339
+ }
340
+ }
341
+ }
342
+ return result
343
+ }
344
+
345
+ /**
346
+ * Extract input mapping from an arrow function
347
+ * Parses: (ref) => ({ key: ref('nodeId', 'path'), key2: 'literal' })
348
+ */
349
+ function extractInputMapping(
350
+ node: ts.Node,
351
+ _checker: ts.TypeChecker
352
+ ): Record<string, unknown | DataRef> {
353
+ if (!ts.isArrowFunction(node)) {
354
+ return {}
355
+ }
356
+
357
+ let bodyObj: ts.ObjectLiteralExpression | undefined
358
+
359
+ if (ts.isObjectLiteralExpression(node.body)) {
360
+ bodyObj = node.body
361
+ } else if (ts.isParenthesizedExpression(node.body)) {
362
+ if (ts.isObjectLiteralExpression(node.body.expression)) {
363
+ bodyObj = node.body.expression
364
+ }
365
+ } else if (ts.isBlock(node.body)) {
366
+ for (const stmt of node.body.statements) {
367
+ if (ts.isReturnStatement(stmt) && stmt.expression) {
368
+ if (ts.isObjectLiteralExpression(stmt.expression)) {
369
+ bodyObj = stmt.expression
370
+ }
371
+ }
372
+ }
373
+ }
374
+
375
+ if (!bodyObj) {
376
+ return {}
377
+ }
378
+
379
+ const refParamName =
380
+ node.parameters.length > 0 && ts.isIdentifier(node.parameters[0].name)
381
+ ? node.parameters[0].name.text
382
+ : 'ref'
383
+
384
+ const input: Record<string, unknown | DataRef> = {}
385
+
386
+ for (const prop of bodyObj.properties) {
387
+ if (!ts.isPropertyAssignment(prop)) continue
388
+
389
+ const key = ts.isIdentifier(prop.name)
390
+ ? prop.name.text
391
+ : ts.isStringLiteral(prop.name)
392
+ ? prop.name.text
393
+ : null
394
+
395
+ if (!key) continue
396
+
397
+ if (ts.isCallExpression(prop.initializer)) {
398
+ const callExpr = prop.initializer.expression
399
+ if (ts.isIdentifier(callExpr) && callExpr.text === refParamName) {
400
+ const args = prop.initializer.arguments
401
+ const nodeIdArg = args[0]
402
+ const pathArg = args[1]
403
+
404
+ const nodeId =
405
+ nodeIdArg && ts.isStringLiteral(nodeIdArg)
406
+ ? nodeIdArg.text
407
+ : 'unknown'
408
+ const path =
409
+ pathArg && ts.isStringLiteral(pathArg) ? pathArg.text : undefined
410
+
411
+ input[key] = { $ref: nodeId, path } as DataRef
412
+ continue
413
+ }
414
+ }
415
+
416
+ if (ts.isStringLiteral(prop.initializer)) {
417
+ input[key] = prop.initializer.text
418
+ } else if (ts.isNumericLiteral(prop.initializer)) {
419
+ input[key] = Number(prop.initializer.text)
420
+ } else if (
421
+ prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
422
+ prop.initializer.kind === ts.SyntaxKind.FalseKeyword
423
+ ) {
424
+ input[key] = prop.initializer.kind === ts.SyntaxKind.TrueKeyword
425
+ } else if (prop.initializer.kind === ts.SyntaxKind.NullKeyword) {
426
+ input[key] = null
427
+ }
428
+ }
429
+
430
+ return input
431
+ }
432
+
433
+ /**
434
+ * Extract next config (string, array, or record)
435
+ */
436
+ function extractNextConfig(
437
+ node: ts.Node,
438
+ _checker: ts.TypeChecker
439
+ ): string | string[] | Record<string, string | string[]> | undefined {
440
+ if (ts.isStringLiteral(node)) {
441
+ return node.text
442
+ }
443
+
444
+ if (ts.isArrayLiteralExpression(node)) {
445
+ return node.elements
446
+ .filter(ts.isStringLiteral)
447
+ .map((el) => (el as ts.StringLiteral).text)
448
+ }
449
+
450
+ if (ts.isObjectLiteralExpression(node)) {
451
+ const result: Record<string, string | string[]> = {}
452
+ for (const prop of node.properties) {
453
+ if (!ts.isPropertyAssignment(prop)) continue
454
+
455
+ const key = ts.isIdentifier(prop.name)
456
+ ? prop.name.text
457
+ : ts.isStringLiteral(prop.name)
458
+ ? prop.name.text
459
+ : null
460
+
461
+ if (!key) continue
462
+
463
+ if (ts.isStringLiteral(prop.initializer)) {
464
+ result[key] = prop.initializer.text
465
+ } else if (ts.isArrayLiteralExpression(prop.initializer)) {
466
+ result[key] = prop.initializer.elements
467
+ .filter(ts.isStringLiteral)
468
+ .map((el) => (el as ts.StringLiteral).text)
469
+ }
470
+ }
471
+ return result
472
+ }
473
+
474
+ return undefined
475
+ }
476
+
477
+ /**
478
+ * Extract definition object from wireWorkflow call
479
+ */
480
+ function extractDefinitionObject(
481
+ firstArg: ts.Node
482
+ ): ts.ObjectLiteralExpression | undefined {
483
+ if (ts.isObjectLiteralExpression(firstArg)) {
484
+ return firstArg
485
+ }
486
+
487
+ if (ts.isArrowFunction(firstArg)) {
488
+ const body = firstArg.body
489
+
490
+ if (ts.isObjectLiteralExpression(body)) {
491
+ return body
492
+ }
493
+
494
+ if (ts.isParenthesizedExpression(body)) {
495
+ if (ts.isObjectLiteralExpression(body.expression)) {
496
+ return body.expression
497
+ }
498
+ }
499
+
500
+ if (ts.isBlock(body)) {
501
+ for (const stmt of body.statements) {
502
+ if (ts.isReturnStatement(stmt) && stmt.expression) {
503
+ if (ts.isObjectLiteralExpression(stmt.expression)) {
504
+ return stmt.expression
505
+ }
506
+ }
507
+ }
508
+ }
509
+ }
510
+
511
+ return undefined
512
+ }
513
+
514
+ /**
515
+ * Compute entry node IDs from graph nodes
516
+ */
517
+ function computeEntryNodeIds(graphNodes: Record<string, any>): string[] {
518
+ const hasIncomingEdge = new Set<string>()
519
+ for (const node of Object.values(graphNodes)) {
520
+ const next = node.next
521
+ if (!next) continue
522
+
523
+ if (typeof next === 'string') {
524
+ hasIncomingEdge.add(next)
525
+ } else if (Array.isArray(next)) {
526
+ next.forEach((n: string) => hasIncomingEdge.add(n))
527
+ } else if (typeof next === 'object') {
528
+ for (const targets of Object.values(next)) {
529
+ if (typeof targets === 'string') {
530
+ hasIncomingEdge.add(targets)
531
+ } else if (Array.isArray(targets)) {
532
+ ;(targets as string[]).forEach((n) => hasIncomingEdge.add(n))
533
+ }
534
+ }
535
+ }
536
+ }
537
+
538
+ return Object.keys(graphNodes).filter(
539
+ (nodeId) => !hasIncomingEdge.has(nodeId)
540
+ )
541
+ }
542
+
543
+ interface PikkuWorkflowGraphExtract {
544
+ name?: string
545
+ description?: string
546
+ tags?: string[]
547
+ wires?: WorkflowWiresConfig
548
+ nodesNode?: ts.ObjectLiteralExpression // The nodes: { entry: 'rpc1', ... } object
549
+ configNode?: ts.ObjectLiteralExpression // The config: { entry: { next: ... }, ... } object
550
+ exportedName?: string
551
+ }
552
+
553
+ /**
554
+ * Extract pikkuWorkflowGraph config from a variable reference or call expression
555
+ * New format: pikkuWorkflowGraph({ nodes: {...}, wires: {...}, config: {...} })
556
+ */
557
+ function extractPikkuWorkflowGraphConfig(
558
+ node: ts.Node,
559
+ checker: ts.TypeChecker
560
+ ): PikkuWorkflowGraphExtract | undefined {
561
+ // If it's an identifier, resolve to the declaration
562
+ if (ts.isIdentifier(node)) {
563
+ const symbol = checker.getSymbolAtLocation(node)
564
+ if (symbol) {
565
+ const declarations = symbol.getDeclarations()
566
+ if (declarations && declarations.length > 0) {
567
+ const decl = declarations[0]
568
+ if (ts.isVariableDeclaration(decl) && decl.initializer) {
569
+ const result = extractPikkuWorkflowGraphConfig(
570
+ decl.initializer,
571
+ checker
572
+ )
573
+ if (result) {
574
+ // Use the variable name as exportedName
575
+ result.exportedName = ts.isIdentifier(decl.name)
576
+ ? decl.name.text
577
+ : undefined
578
+ }
579
+ return result
580
+ }
581
+ }
582
+ }
583
+ return undefined
584
+ }
585
+
586
+ // If it's a call expression to pikkuWorkflowGraph
587
+ if (ts.isCallExpression(node)) {
588
+ const expr = node.expression
589
+ if (ts.isIdentifier(expr) && expr.text === 'pikkuWorkflowGraph') {
590
+ const configArg = node.arguments[0]
591
+ if (configArg && ts.isObjectLiteralExpression(configArg)) {
592
+ let name: string | undefined
593
+ let description: string | undefined
594
+ let tags: string[] | undefined
595
+ let wires: WorkflowWiresConfig | undefined
596
+ let nodesNode: ts.ObjectLiteralExpression | undefined
597
+ let configNode: ts.ObjectLiteralExpression | undefined
598
+
599
+ for (const prop of configArg.properties) {
600
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
601
+ continue
602
+
603
+ const propName = prop.name.text
604
+ if (propName === 'name') {
605
+ name = extractStringLiteral(prop.initializer, checker)
606
+ } else if (propName === 'description') {
607
+ description = extractStringLiteral(prop.initializer, checker)
608
+ } else if (
609
+ propName === 'tags' &&
610
+ ts.isArrayLiteralExpression(prop.initializer)
611
+ ) {
612
+ tags = prop.initializer.elements
613
+ .filter(ts.isStringLiteral)
614
+ .map((el) => (el as ts.StringLiteral).text)
615
+ } else if (
616
+ propName === 'wires' &&
617
+ ts.isObjectLiteralExpression(prop.initializer)
618
+ ) {
619
+ wires = extractWiresConfig(prop.initializer, checker)
620
+ } else if (
621
+ propName === 'nodes' &&
622
+ ts.isObjectLiteralExpression(prop.initializer)
623
+ ) {
624
+ nodesNode = prop.initializer
625
+ } else if (
626
+ propName === 'config' &&
627
+ ts.isObjectLiteralExpression(prop.initializer)
628
+ ) {
629
+ configNode = prop.initializer
630
+ }
631
+ }
632
+
633
+ return { name, description, tags, wires, nodesNode, configNode }
634
+ }
635
+ }
636
+ }
637
+
638
+ return undefined
639
+ }
640
+
641
+ /**
642
+ * Extract graph nodes from the new pikkuWorkflowGraph format
643
+ * New format: { nodes: { entry: 'rpcName', ... }, config: { entry: { next: 'sendWelcome', ... }, ... } }
644
+ */
645
+ function extractGraphFromNewFormat(
646
+ nodesNode: ts.ObjectLiteralExpression | undefined,
647
+ configNode: ts.ObjectLiteralExpression | undefined,
648
+ checker: ts.TypeChecker,
649
+ state: any
650
+ ): Record<string, any> {
651
+ const nodes: Record<string, any> = {}
652
+
653
+ if (!nodesNode) {
654
+ return nodes
655
+ }
656
+
657
+ // Extract node ID to RPC name mapping from 'nodes' property
658
+ const nodeRpcMap: Record<string, string> = {}
659
+ for (const prop of nodesNode.properties) {
660
+ if (!ts.isPropertyAssignment(prop)) continue
661
+
662
+ const nodeId = ts.isIdentifier(prop.name)
663
+ ? prop.name.text
664
+ : ts.isStringLiteral(prop.name)
665
+ ? prop.name.text
666
+ : null
667
+
668
+ if (!nodeId) continue
669
+
670
+ const rpcName = extractStringLiteral(prop.initializer, checker)
671
+ if (rpcName) {
672
+ nodeRpcMap[nodeId] = rpcName
673
+ state.rpc.invokedFunctions.add(rpcName)
674
+ }
675
+ }
676
+
677
+ // Initialize nodes with their RPC names
678
+ for (const [nodeId, rpcName] of Object.entries(nodeRpcMap)) {
679
+ nodes[nodeId] = {
680
+ nodeId,
681
+ rpcName,
682
+ input: {},
683
+ next: undefined,
684
+ onError: undefined,
685
+ }
686
+ }
687
+
688
+ // Extract config for each node from 'config' property
689
+ if (configNode) {
690
+ for (const prop of configNode.properties) {
691
+ if (!ts.isPropertyAssignment(prop)) continue
692
+
693
+ const nodeId = ts.isIdentifier(prop.name)
694
+ ? prop.name.text
695
+ : ts.isStringLiteral(prop.name)
696
+ ? prop.name.text
697
+ : null
698
+
699
+ if (!nodeId || !nodes[nodeId]) continue
700
+
701
+ if (ts.isObjectLiteralExpression(prop.initializer)) {
702
+ const nodeConfig = extractNodeConfigFromObject(
703
+ prop.initializer,
704
+ checker
705
+ )
706
+ if (nodeConfig) {
707
+ nodes[nodeId].next = nodeConfig.next
708
+ nodes[nodeId].onError = nodeConfig.onError
709
+ nodes[nodeId].input = nodeConfig.input
710
+ }
711
+ }
712
+ }
713
+ }
714
+
715
+ return nodes
716
+ }
717
+
718
+ /**
719
+ * Extract node config (next, onError, input) from object literal
720
+ */
721
+ function extractNodeConfigFromObject(
722
+ obj: ts.ObjectLiteralExpression,
723
+ checker: ts.TypeChecker
724
+ ): {
725
+ next: any
726
+ onError: any
727
+ input: Record<string, any>
728
+ } {
729
+ let next: any = undefined
730
+ let onError: any = undefined
731
+ let input: Record<string, any> = {}
732
+
733
+ for (const prop of obj.properties) {
734
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue
735
+
736
+ const propName = prop.name.text
737
+
738
+ if (propName === 'next') {
739
+ next = extractNextConfig(prop.initializer, checker)
740
+ } else if (propName === 'onError') {
741
+ onError = extractNextConfig(prop.initializer, checker)
742
+ } else if (propName === 'input') {
743
+ input = extractInputMapping(prop.initializer, checker)
744
+ }
745
+ }
746
+
747
+ return { next, onError, input }
748
+ }
749
+
750
+ /**
751
+ * Inspector for wireWorkflow() calls with graph definitions
752
+ * Detects: wireWorkflow({ wires: {...}, graph: pikkuWorkflowGraphResult })
753
+ */
754
+ export const addWorkflowGraph: AddWiring = (logger, node, checker, state) => {
755
+ if (!ts.isCallExpression(node)) {
756
+ return
757
+ }
758
+
759
+ const expression = node.expression
760
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireWorkflow') {
761
+ return
762
+ }
763
+
764
+ const args = node.arguments
765
+ const firstArg = args[0]
766
+
767
+ if (!firstArg) {
768
+ logger.critical(ErrorCode.MISSING_FUNC, 'wireWorkflow requires an argument')
769
+ return
770
+ }
771
+
772
+ const definitionObj = extractDefinitionObject(firstArg)
773
+
774
+ if (!definitionObj) {
775
+ logger.critical(
776
+ ErrorCode.MISSING_FUNC,
777
+ 'wireWorkflow requires an object argument'
778
+ )
779
+ return
780
+ }
781
+
782
+ // Check if this is a graph workflow (has 'graph' property)
783
+ let graphPropNode: ts.Node | undefined
784
+ let enabled = true // Default to true
785
+
786
+ for (const prop of definitionObj.properties) {
787
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue
788
+
789
+ const propName = prop.name.text
790
+ if (propName === 'graph') {
791
+ graphPropNode = prop.initializer
792
+ } else if (propName === 'enabled') {
793
+ // Check if enabled is explicitly set to false
794
+ if (prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
795
+ enabled = false
796
+ }
797
+ }
798
+ // Note: We no longer extract wires from wireWorkflow - they come from pikkuWorkflowGraph
799
+ }
800
+
801
+ // If no graph property, this is a DSL workflow - skip (handled by add-workflow.ts)
802
+ if (!graphPropNode) {
803
+ return
804
+ }
805
+
806
+ // Extract config from the pikkuWorkflowGraph result
807
+ const graphConfig = extractPikkuWorkflowGraphConfig(graphPropNode, checker)
808
+
809
+ if (!graphConfig) {
810
+ logger.critical(
811
+ ErrorCode.MISSING_NAME,
812
+ 'wireWorkflow with graph requires a pikkuWorkflowGraph'
813
+ )
814
+ return
815
+ }
816
+
817
+ // Use explicit name or fall back to exported variable name
818
+ const workflowName = graphConfig.name || graphConfig.exportedName
819
+
820
+ if (!workflowName) {
821
+ logger.critical(
822
+ ErrorCode.MISSING_NAME,
823
+ 'wireWorkflow with graph requires a pikkuWorkflowGraph with a name property or exported variable name'
824
+ )
825
+ return
826
+ }
827
+
828
+ // Extract graph nodes from the new format (nodes + config properties)
829
+ let graphNodes: Record<string, any> = {}
830
+ if (graphConfig.nodesNode) {
831
+ graphNodes = extractGraphFromNewFormat(
832
+ graphConfig.nodesNode,
833
+ graphConfig.configNode,
834
+ checker,
835
+ state
836
+ )
837
+ }
838
+
839
+ const entryNodeIds = computeEntryNodeIds(graphNodes)
840
+
841
+ // Use wires from pikkuWorkflowGraph (not from wireWorkflow)
842
+ const wires = graphConfig.wires || {}
843
+
844
+ const serialized: SerializedWorkflowGraph = {
845
+ name: workflowName,
846
+ pikkuFuncName: workflowName,
847
+ source: 'graph',
848
+ description: graphConfig.description,
849
+ tags: graphConfig.tags,
850
+ wires,
851
+ nodes: graphNodes,
852
+ entryNodeIds,
853
+ }
854
+
855
+ // Only add if enabled (or not explicitly disabled)
856
+ if (enabled) {
857
+ state.workflows.graphMeta[workflowName] = serialized
858
+ // Store file path and exported name for import generation
859
+ state.workflows.graphFiles.set(workflowName, {
860
+ path: node.getSourceFile().fileName,
861
+ exportedName: graphConfig.exportedName || workflowName,
862
+ })
863
+ }
864
+ }