@pikku/inspector 0.11.1 → 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 (68) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/dist/add/add-forge-credential.d.ts +8 -0
  3. package/dist/add/add-forge-credential.js +77 -0
  4. package/dist/add/add-forge-node.d.ts +7 -0
  5. package/dist/add/add-forge-node.js +77 -0
  6. package/dist/add/add-functions.js +102 -9
  7. package/dist/add/add-http-route.js +24 -1
  8. package/dist/add/add-rpc-invocations.d.ts +3 -0
  9. package/dist/add/add-rpc-invocations.js +51 -25
  10. package/dist/add/add-workflow-graph.d.ts +6 -0
  11. package/dist/add/add-workflow-graph.js +659 -0
  12. package/dist/add/add-workflow.js +118 -22
  13. package/dist/error-codes.d.ts +3 -1
  14. package/dist/error-codes.js +3 -1
  15. package/dist/index.d.ts +3 -0
  16. package/dist/index.js +2 -0
  17. package/dist/inspector.js +19 -3
  18. package/dist/types.d.ts +26 -0
  19. package/dist/utils/extract-function-name.js +7 -7
  20. package/dist/utils/get-property-value.d.ts +2 -1
  21. package/dist/utils/get-property-value.js +6 -2
  22. package/dist/utils/serialize-inspector-state.d.ts +24 -1
  23. package/dist/utils/serialize-inspector-state.js +24 -0
  24. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.d.ts +24 -0
  25. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +898 -0
  26. package/dist/{workflow/extract-simple-workflow.d.ts → utils/workflow/dsl/extract-dsl-workflow.d.ts} +4 -2
  27. package/dist/{workflow/extract-simple-workflow.js → utils/workflow/dsl/extract-dsl-workflow.js} +549 -68
  28. package/dist/utils/workflow/dsl/index.d.ts +7 -0
  29. package/dist/utils/workflow/dsl/index.js +7 -0
  30. package/dist/{workflow → utils/workflow/dsl}/patterns.d.ts +21 -0
  31. package/dist/{workflow → utils/workflow/dsl}/patterns.js +90 -10
  32. package/dist/{workflow → utils/workflow/dsl}/validation.d.ts +2 -0
  33. package/dist/{workflow → utils/workflow/dsl}/validation.js +25 -7
  34. package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +13 -0
  35. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +316 -0
  36. package/dist/utils/workflow/graph/index.d.ts +6 -0
  37. package/dist/utils/workflow/graph/index.js +6 -0
  38. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +43 -0
  39. package/dist/utils/workflow/graph/serialize-workflow-graph.js +152 -0
  40. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +229 -0
  41. package/dist/utils/workflow/graph/workflow-graph.types.js +38 -0
  42. package/dist/visit.js +6 -0
  43. package/package.json +14 -2
  44. package/src/add/add-forge-credential.ts +119 -0
  45. package/src/add/add-forge-node.ts +132 -0
  46. package/src/add/add-functions.ts +129 -15
  47. package/src/add/add-http-route.ts +25 -1
  48. package/src/add/add-rpc-invocations.ts +61 -31
  49. package/src/add/add-workflow-graph.ts +864 -0
  50. package/src/add/add-workflow.ts +112 -26
  51. package/src/error-codes.ts +3 -1
  52. package/src/index.ts +10 -0
  53. package/src/inspector.ts +20 -4
  54. package/src/types.ts +25 -1
  55. package/src/utils/extract-function-name.ts +7 -7
  56. package/src/utils/get-property-value.ts +9 -2
  57. package/src/utils/serialize-inspector-state.ts +39 -1
  58. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +1180 -0
  59. package/src/{workflow/extract-simple-workflow.ts → utils/workflow/dsl/extract-dsl-workflow.ts} +654 -81
  60. package/src/utils/workflow/dsl/index.ts +11 -0
  61. package/src/{workflow → utils/workflow/dsl}/patterns.ts +108 -11
  62. package/src/{workflow → utils/workflow/dsl}/validation.ts +34 -7
  63. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +415 -0
  64. package/src/utils/workflow/graph/index.ts +6 -0
  65. package/src/utils/workflow/graph/serialize-workflow-graph.ts +223 -0
  66. package/src/utils/workflow/graph/workflow-graph.types.ts +280 -0
  67. package/src/visit.ts +6 -0
  68. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,659 @@
1
+ import * as ts from 'typescript';
2
+ import { ErrorCode } from '../error-codes.js';
3
+ import { extractStringLiteral } from '../utils/extract-node-value.js';
4
+ /**
5
+ * Extract wire configuration from object literal
6
+ */
7
+ function extractWiresConfig(wiresNode, checker) {
8
+ const wires = {};
9
+ for (const prop of wiresNode.properties) {
10
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
11
+ continue;
12
+ const propName = prop.name.text;
13
+ if (propName === 'http' && ts.isArrayLiteralExpression(prop.initializer)) {
14
+ wires.http = [];
15
+ for (const elem of prop.initializer.elements) {
16
+ if (ts.isObjectLiteralExpression(elem)) {
17
+ const httpWire = {};
18
+ for (const httpProp of elem.properties) {
19
+ if (!ts.isPropertyAssignment(httpProp) ||
20
+ !ts.isIdentifier(httpProp.name))
21
+ continue;
22
+ const httpPropName = httpProp.name.text;
23
+ if (httpPropName === 'route') {
24
+ httpWire.route = extractStringLiteral(httpProp.initializer, checker);
25
+ }
26
+ else if (httpPropName === 'method') {
27
+ httpWire.method = extractStringLiteral(httpProp.initializer, checker);
28
+ }
29
+ else if (httpPropName === 'startNode') {
30
+ httpWire.startNode = extractStringLiteral(httpProp.initializer, checker);
31
+ }
32
+ }
33
+ if (httpWire.route && httpWire.method && httpWire.startNode) {
34
+ wires.http.push(httpWire);
35
+ }
36
+ }
37
+ }
38
+ }
39
+ else if (propName === 'channel' &&
40
+ ts.isArrayLiteralExpression(prop.initializer)) {
41
+ wires.channel = [];
42
+ for (const elem of prop.initializer.elements) {
43
+ if (ts.isObjectLiteralExpression(elem)) {
44
+ const channelWire = {};
45
+ for (const channelProp of elem.properties) {
46
+ if (!ts.isPropertyAssignment(channelProp) ||
47
+ !ts.isIdentifier(channelProp.name))
48
+ continue;
49
+ const channelPropName = channelProp.name.text;
50
+ if (channelPropName === 'name') {
51
+ channelWire.name = extractStringLiteral(channelProp.initializer, checker);
52
+ }
53
+ else if (channelPropName === 'onConnect') {
54
+ channelWire.onConnect = extractStringLiteral(channelProp.initializer, checker);
55
+ }
56
+ else if (channelPropName === 'onDisconnect') {
57
+ channelWire.onDisconnect = extractStringLiteral(channelProp.initializer, checker);
58
+ }
59
+ else if (channelPropName === 'onMessage') {
60
+ channelWire.onMessage = extractStringLiteral(channelProp.initializer, checker);
61
+ }
62
+ }
63
+ if (channelWire.name) {
64
+ wires.channel.push(channelWire);
65
+ }
66
+ }
67
+ }
68
+ }
69
+ else if (propName === 'queue' &&
70
+ ts.isArrayLiteralExpression(prop.initializer)) {
71
+ wires.queue = [];
72
+ for (const elem of prop.initializer.elements) {
73
+ if (ts.isObjectLiteralExpression(elem)) {
74
+ const queueWire = {};
75
+ for (const queueProp of elem.properties) {
76
+ if (!ts.isPropertyAssignment(queueProp) ||
77
+ !ts.isIdentifier(queueProp.name))
78
+ continue;
79
+ const queuePropName = queueProp.name.text;
80
+ if (queuePropName === 'name') {
81
+ queueWire.name = extractStringLiteral(queueProp.initializer, checker);
82
+ }
83
+ else if (queuePropName === 'startNode') {
84
+ queueWire.startNode = extractStringLiteral(queueProp.initializer, checker);
85
+ }
86
+ }
87
+ if (queueWire.name && queueWire.startNode) {
88
+ wires.queue.push(queueWire);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ else if (propName === 'cli' &&
94
+ ts.isArrayLiteralExpression(prop.initializer)) {
95
+ wires.cli = [];
96
+ for (const elem of prop.initializer.elements) {
97
+ if (ts.isObjectLiteralExpression(elem)) {
98
+ const cliWire = {};
99
+ for (const cliProp of elem.properties) {
100
+ if (!ts.isPropertyAssignment(cliProp) ||
101
+ !ts.isIdentifier(cliProp.name))
102
+ continue;
103
+ const cliPropName = cliProp.name.text;
104
+ if (cliPropName === 'command') {
105
+ cliWire.command = extractStringLiteral(cliProp.initializer, checker);
106
+ }
107
+ else if (cliPropName === 'startNode') {
108
+ cliWire.startNode = extractStringLiteral(cliProp.initializer, checker);
109
+ }
110
+ }
111
+ if (cliWire.command && cliWire.startNode) {
112
+ wires.cli.push(cliWire);
113
+ }
114
+ }
115
+ }
116
+ }
117
+ else if (propName === 'mcp' &&
118
+ ts.isObjectLiteralExpression(prop.initializer)) {
119
+ const mcpWires = {};
120
+ for (const mcpProp of prop.initializer.properties) {
121
+ if (!ts.isPropertyAssignment(mcpProp) || !ts.isIdentifier(mcpProp.name))
122
+ continue;
123
+ const mcpPropName = mcpProp.name.text;
124
+ if (mcpPropName === 'tool' &&
125
+ ts.isArrayLiteralExpression(mcpProp.initializer)) {
126
+ mcpWires.tool = extractMcpToolWireArray(mcpProp.initializer, checker);
127
+ }
128
+ else if (mcpPropName === 'prompt' &&
129
+ ts.isArrayLiteralExpression(mcpProp.initializer)) {
130
+ mcpWires.prompt = extractMcpToolWireArray(mcpProp.initializer, checker);
131
+ }
132
+ else if (mcpPropName === 'resource' &&
133
+ ts.isArrayLiteralExpression(mcpProp.initializer)) {
134
+ mcpWires.resource = extractMcpResourceWireArray(mcpProp.initializer, checker);
135
+ }
136
+ }
137
+ if (mcpWires.tool || mcpWires.prompt || mcpWires.resource) {
138
+ wires.mcp = mcpWires;
139
+ }
140
+ }
141
+ else if (propName === 'schedule' &&
142
+ ts.isArrayLiteralExpression(prop.initializer)) {
143
+ wires.schedule = [];
144
+ for (const elem of prop.initializer.elements) {
145
+ if (ts.isObjectLiteralExpression(elem)) {
146
+ const scheduleWire = {};
147
+ for (const scheduleProp of elem.properties) {
148
+ if (!ts.isPropertyAssignment(scheduleProp) ||
149
+ !ts.isIdentifier(scheduleProp.name))
150
+ continue;
151
+ const schedulePropName = scheduleProp.name.text;
152
+ if (schedulePropName === 'cron') {
153
+ scheduleWire.cron = extractStringLiteral(scheduleProp.initializer, checker);
154
+ }
155
+ else if (schedulePropName === 'interval') {
156
+ scheduleWire.interval = extractStringLiteral(scheduleProp.initializer, checker);
157
+ }
158
+ else if (schedulePropName === 'startNode') {
159
+ scheduleWire.startNode = extractStringLiteral(scheduleProp.initializer, checker);
160
+ }
161
+ }
162
+ if ((scheduleWire.cron || scheduleWire.interval) &&
163
+ scheduleWire.startNode) {
164
+ wires.schedule.push(scheduleWire);
165
+ }
166
+ }
167
+ }
168
+ }
169
+ else if (propName === 'trigger' &&
170
+ ts.isArrayLiteralExpression(prop.initializer)) {
171
+ wires.trigger = [];
172
+ for (const elem of prop.initializer.elements) {
173
+ if (ts.isObjectLiteralExpression(elem)) {
174
+ const triggerWire = {};
175
+ for (const triggerProp of elem.properties) {
176
+ if (!ts.isPropertyAssignment(triggerProp) ||
177
+ !ts.isIdentifier(triggerProp.name))
178
+ continue;
179
+ const triggerPropName = triggerProp.name.text;
180
+ if (triggerPropName === 'name') {
181
+ triggerWire.name = extractStringLiteral(triggerProp.initializer, checker);
182
+ }
183
+ else if (triggerPropName === 'startNode') {
184
+ triggerWire.startNode = extractStringLiteral(triggerProp.initializer, checker);
185
+ }
186
+ }
187
+ if (triggerWire.name && triggerWire.startNode) {
188
+ wires.trigger.push(triggerWire);
189
+ }
190
+ }
191
+ }
192
+ }
193
+ }
194
+ return wires;
195
+ }
196
+ /**
197
+ * Helper to extract MCP wire arrays for tool/prompt (name field)
198
+ */
199
+ function extractMcpToolWireArray(arrayNode, checker) {
200
+ const result = [];
201
+ for (const elem of arrayNode.elements) {
202
+ if (ts.isObjectLiteralExpression(elem)) {
203
+ let name;
204
+ let startNode;
205
+ for (const prop of elem.properties) {
206
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
207
+ continue;
208
+ const propName = prop.name.text;
209
+ if (propName === 'name') {
210
+ name = extractStringLiteral(prop.initializer, checker);
211
+ }
212
+ else if (propName === 'startNode') {
213
+ startNode = extractStringLiteral(prop.initializer, checker);
214
+ }
215
+ }
216
+ if (name && startNode) {
217
+ result.push({ name, startNode });
218
+ }
219
+ }
220
+ }
221
+ return result;
222
+ }
223
+ /**
224
+ * Helper to extract MCP wire arrays for resource (uri field)
225
+ */
226
+ function extractMcpResourceWireArray(arrayNode, checker) {
227
+ const result = [];
228
+ for (const elem of arrayNode.elements) {
229
+ if (ts.isObjectLiteralExpression(elem)) {
230
+ let uri;
231
+ let startNode;
232
+ for (const prop of elem.properties) {
233
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
234
+ continue;
235
+ const propName = prop.name.text;
236
+ if (propName === 'uri') {
237
+ uri = extractStringLiteral(prop.initializer, checker);
238
+ }
239
+ else if (propName === 'startNode') {
240
+ startNode = extractStringLiteral(prop.initializer, checker);
241
+ }
242
+ }
243
+ if (uri && startNode) {
244
+ result.push({ uri, startNode });
245
+ }
246
+ }
247
+ }
248
+ return result;
249
+ }
250
+ /**
251
+ * Extract input mapping from an arrow function
252
+ * Parses: (ref) => ({ key: ref('nodeId', 'path'), key2: 'literal' })
253
+ */
254
+ function extractInputMapping(node, _checker) {
255
+ if (!ts.isArrowFunction(node)) {
256
+ return {};
257
+ }
258
+ let bodyObj;
259
+ if (ts.isObjectLiteralExpression(node.body)) {
260
+ bodyObj = node.body;
261
+ }
262
+ else if (ts.isParenthesizedExpression(node.body)) {
263
+ if (ts.isObjectLiteralExpression(node.body.expression)) {
264
+ bodyObj = node.body.expression;
265
+ }
266
+ }
267
+ else if (ts.isBlock(node.body)) {
268
+ for (const stmt of node.body.statements) {
269
+ if (ts.isReturnStatement(stmt) && stmt.expression) {
270
+ if (ts.isObjectLiteralExpression(stmt.expression)) {
271
+ bodyObj = stmt.expression;
272
+ }
273
+ }
274
+ }
275
+ }
276
+ if (!bodyObj) {
277
+ return {};
278
+ }
279
+ const refParamName = node.parameters.length > 0 && ts.isIdentifier(node.parameters[0].name)
280
+ ? node.parameters[0].name.text
281
+ : 'ref';
282
+ const input = {};
283
+ for (const prop of bodyObj.properties) {
284
+ if (!ts.isPropertyAssignment(prop))
285
+ continue;
286
+ const key = ts.isIdentifier(prop.name)
287
+ ? prop.name.text
288
+ : ts.isStringLiteral(prop.name)
289
+ ? prop.name.text
290
+ : null;
291
+ if (!key)
292
+ continue;
293
+ if (ts.isCallExpression(prop.initializer)) {
294
+ const callExpr = prop.initializer.expression;
295
+ if (ts.isIdentifier(callExpr) && callExpr.text === refParamName) {
296
+ const args = prop.initializer.arguments;
297
+ const nodeIdArg = args[0];
298
+ const pathArg = args[1];
299
+ const nodeId = nodeIdArg && ts.isStringLiteral(nodeIdArg)
300
+ ? nodeIdArg.text
301
+ : 'unknown';
302
+ const path = pathArg && ts.isStringLiteral(pathArg) ? pathArg.text : undefined;
303
+ input[key] = { $ref: nodeId, path };
304
+ continue;
305
+ }
306
+ }
307
+ if (ts.isStringLiteral(prop.initializer)) {
308
+ input[key] = prop.initializer.text;
309
+ }
310
+ else if (ts.isNumericLiteral(prop.initializer)) {
311
+ input[key] = Number(prop.initializer.text);
312
+ }
313
+ else if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
314
+ prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
315
+ input[key] = prop.initializer.kind === ts.SyntaxKind.TrueKeyword;
316
+ }
317
+ else if (prop.initializer.kind === ts.SyntaxKind.NullKeyword) {
318
+ input[key] = null;
319
+ }
320
+ }
321
+ return input;
322
+ }
323
+ /**
324
+ * Extract next config (string, array, or record)
325
+ */
326
+ function extractNextConfig(node, _checker) {
327
+ if (ts.isStringLiteral(node)) {
328
+ return node.text;
329
+ }
330
+ if (ts.isArrayLiteralExpression(node)) {
331
+ return node.elements
332
+ .filter(ts.isStringLiteral)
333
+ .map((el) => el.text);
334
+ }
335
+ if (ts.isObjectLiteralExpression(node)) {
336
+ const result = {};
337
+ for (const prop of node.properties) {
338
+ if (!ts.isPropertyAssignment(prop))
339
+ continue;
340
+ const key = ts.isIdentifier(prop.name)
341
+ ? prop.name.text
342
+ : ts.isStringLiteral(prop.name)
343
+ ? prop.name.text
344
+ : null;
345
+ if (!key)
346
+ continue;
347
+ if (ts.isStringLiteral(prop.initializer)) {
348
+ result[key] = prop.initializer.text;
349
+ }
350
+ else if (ts.isArrayLiteralExpression(prop.initializer)) {
351
+ result[key] = prop.initializer.elements
352
+ .filter(ts.isStringLiteral)
353
+ .map((el) => el.text);
354
+ }
355
+ }
356
+ return result;
357
+ }
358
+ return undefined;
359
+ }
360
+ /**
361
+ * Extract definition object from wireWorkflow call
362
+ */
363
+ function extractDefinitionObject(firstArg) {
364
+ if (ts.isObjectLiteralExpression(firstArg)) {
365
+ return firstArg;
366
+ }
367
+ if (ts.isArrowFunction(firstArg)) {
368
+ const body = firstArg.body;
369
+ if (ts.isObjectLiteralExpression(body)) {
370
+ return body;
371
+ }
372
+ if (ts.isParenthesizedExpression(body)) {
373
+ if (ts.isObjectLiteralExpression(body.expression)) {
374
+ return body.expression;
375
+ }
376
+ }
377
+ if (ts.isBlock(body)) {
378
+ for (const stmt of body.statements) {
379
+ if (ts.isReturnStatement(stmt) && stmt.expression) {
380
+ if (ts.isObjectLiteralExpression(stmt.expression)) {
381
+ return stmt.expression;
382
+ }
383
+ }
384
+ }
385
+ }
386
+ }
387
+ return undefined;
388
+ }
389
+ /**
390
+ * Compute entry node IDs from graph nodes
391
+ */
392
+ function computeEntryNodeIds(graphNodes) {
393
+ const hasIncomingEdge = new Set();
394
+ for (const node of Object.values(graphNodes)) {
395
+ const next = node.next;
396
+ if (!next)
397
+ continue;
398
+ if (typeof next === 'string') {
399
+ hasIncomingEdge.add(next);
400
+ }
401
+ else if (Array.isArray(next)) {
402
+ next.forEach((n) => hasIncomingEdge.add(n));
403
+ }
404
+ else if (typeof next === 'object') {
405
+ for (const targets of Object.values(next)) {
406
+ if (typeof targets === 'string') {
407
+ hasIncomingEdge.add(targets);
408
+ }
409
+ else if (Array.isArray(targets)) {
410
+ ;
411
+ targets.forEach((n) => hasIncomingEdge.add(n));
412
+ }
413
+ }
414
+ }
415
+ }
416
+ return Object.keys(graphNodes).filter((nodeId) => !hasIncomingEdge.has(nodeId));
417
+ }
418
+ /**
419
+ * Extract pikkuWorkflowGraph config from a variable reference or call expression
420
+ * New format: pikkuWorkflowGraph({ nodes: {...}, wires: {...}, config: {...} })
421
+ */
422
+ function extractPikkuWorkflowGraphConfig(node, checker) {
423
+ // If it's an identifier, resolve to the declaration
424
+ if (ts.isIdentifier(node)) {
425
+ const symbol = checker.getSymbolAtLocation(node);
426
+ if (symbol) {
427
+ const declarations = symbol.getDeclarations();
428
+ if (declarations && declarations.length > 0) {
429
+ const decl = declarations[0];
430
+ if (ts.isVariableDeclaration(decl) && decl.initializer) {
431
+ const result = extractPikkuWorkflowGraphConfig(decl.initializer, checker);
432
+ if (result) {
433
+ // Use the variable name as exportedName
434
+ result.exportedName = ts.isIdentifier(decl.name)
435
+ ? decl.name.text
436
+ : undefined;
437
+ }
438
+ return result;
439
+ }
440
+ }
441
+ }
442
+ return undefined;
443
+ }
444
+ // If it's a call expression to pikkuWorkflowGraph
445
+ if (ts.isCallExpression(node)) {
446
+ const expr = node.expression;
447
+ if (ts.isIdentifier(expr) && expr.text === 'pikkuWorkflowGraph') {
448
+ const configArg = node.arguments[0];
449
+ if (configArg && ts.isObjectLiteralExpression(configArg)) {
450
+ let name;
451
+ let description;
452
+ let tags;
453
+ let wires;
454
+ let nodesNode;
455
+ let configNode;
456
+ for (const prop of configArg.properties) {
457
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
458
+ continue;
459
+ const propName = prop.name.text;
460
+ if (propName === 'name') {
461
+ name = extractStringLiteral(prop.initializer, checker);
462
+ }
463
+ else if (propName === 'description') {
464
+ description = extractStringLiteral(prop.initializer, checker);
465
+ }
466
+ else if (propName === 'tags' &&
467
+ ts.isArrayLiteralExpression(prop.initializer)) {
468
+ tags = prop.initializer.elements
469
+ .filter(ts.isStringLiteral)
470
+ .map((el) => el.text);
471
+ }
472
+ else if (propName === 'wires' &&
473
+ ts.isObjectLiteralExpression(prop.initializer)) {
474
+ wires = extractWiresConfig(prop.initializer, checker);
475
+ }
476
+ else if (propName === 'nodes' &&
477
+ ts.isObjectLiteralExpression(prop.initializer)) {
478
+ nodesNode = prop.initializer;
479
+ }
480
+ else if (propName === 'config' &&
481
+ ts.isObjectLiteralExpression(prop.initializer)) {
482
+ configNode = prop.initializer;
483
+ }
484
+ }
485
+ return { name, description, tags, wires, nodesNode, configNode };
486
+ }
487
+ }
488
+ }
489
+ return undefined;
490
+ }
491
+ /**
492
+ * Extract graph nodes from the new pikkuWorkflowGraph format
493
+ * New format: { nodes: { entry: 'rpcName', ... }, config: { entry: { next: 'sendWelcome', ... }, ... } }
494
+ */
495
+ function extractGraphFromNewFormat(nodesNode, configNode, checker, state) {
496
+ const nodes = {};
497
+ if (!nodesNode) {
498
+ return nodes;
499
+ }
500
+ // Extract node ID to RPC name mapping from 'nodes' property
501
+ const nodeRpcMap = {};
502
+ for (const prop of nodesNode.properties) {
503
+ if (!ts.isPropertyAssignment(prop))
504
+ continue;
505
+ const nodeId = ts.isIdentifier(prop.name)
506
+ ? prop.name.text
507
+ : ts.isStringLiteral(prop.name)
508
+ ? prop.name.text
509
+ : null;
510
+ if (!nodeId)
511
+ continue;
512
+ const rpcName = extractStringLiteral(prop.initializer, checker);
513
+ if (rpcName) {
514
+ nodeRpcMap[nodeId] = rpcName;
515
+ state.rpc.invokedFunctions.add(rpcName);
516
+ }
517
+ }
518
+ // Initialize nodes with their RPC names
519
+ for (const [nodeId, rpcName] of Object.entries(nodeRpcMap)) {
520
+ nodes[nodeId] = {
521
+ nodeId,
522
+ rpcName,
523
+ input: {},
524
+ next: undefined,
525
+ onError: undefined,
526
+ };
527
+ }
528
+ // Extract config for each node from 'config' property
529
+ if (configNode) {
530
+ for (const prop of configNode.properties) {
531
+ if (!ts.isPropertyAssignment(prop))
532
+ continue;
533
+ const nodeId = ts.isIdentifier(prop.name)
534
+ ? prop.name.text
535
+ : ts.isStringLiteral(prop.name)
536
+ ? prop.name.text
537
+ : null;
538
+ if (!nodeId || !nodes[nodeId])
539
+ continue;
540
+ if (ts.isObjectLiteralExpression(prop.initializer)) {
541
+ const nodeConfig = extractNodeConfigFromObject(prop.initializer, checker);
542
+ if (nodeConfig) {
543
+ nodes[nodeId].next = nodeConfig.next;
544
+ nodes[nodeId].onError = nodeConfig.onError;
545
+ nodes[nodeId].input = nodeConfig.input;
546
+ }
547
+ }
548
+ }
549
+ }
550
+ return nodes;
551
+ }
552
+ /**
553
+ * Extract node config (next, onError, input) from object literal
554
+ */
555
+ function extractNodeConfigFromObject(obj, checker) {
556
+ let next = undefined;
557
+ let onError = undefined;
558
+ let input = {};
559
+ for (const prop of obj.properties) {
560
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
561
+ continue;
562
+ const propName = prop.name.text;
563
+ if (propName === 'next') {
564
+ next = extractNextConfig(prop.initializer, checker);
565
+ }
566
+ else if (propName === 'onError') {
567
+ onError = extractNextConfig(prop.initializer, checker);
568
+ }
569
+ else if (propName === 'input') {
570
+ input = extractInputMapping(prop.initializer, checker);
571
+ }
572
+ }
573
+ return { next, onError, input };
574
+ }
575
+ /**
576
+ * Inspector for wireWorkflow() calls with graph definitions
577
+ * Detects: wireWorkflow({ wires: {...}, graph: pikkuWorkflowGraphResult })
578
+ */
579
+ export const addWorkflowGraph = (logger, node, checker, state) => {
580
+ if (!ts.isCallExpression(node)) {
581
+ return;
582
+ }
583
+ const expression = node.expression;
584
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireWorkflow') {
585
+ return;
586
+ }
587
+ const args = node.arguments;
588
+ const firstArg = args[0];
589
+ if (!firstArg) {
590
+ logger.critical(ErrorCode.MISSING_FUNC, 'wireWorkflow requires an argument');
591
+ return;
592
+ }
593
+ const definitionObj = extractDefinitionObject(firstArg);
594
+ if (!definitionObj) {
595
+ logger.critical(ErrorCode.MISSING_FUNC, 'wireWorkflow requires an object argument');
596
+ return;
597
+ }
598
+ // Check if this is a graph workflow (has 'graph' property)
599
+ let graphPropNode;
600
+ let enabled = true; // Default to true
601
+ for (const prop of definitionObj.properties) {
602
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
603
+ continue;
604
+ const propName = prop.name.text;
605
+ if (propName === 'graph') {
606
+ graphPropNode = prop.initializer;
607
+ }
608
+ else if (propName === 'enabled') {
609
+ // Check if enabled is explicitly set to false
610
+ if (prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
611
+ enabled = false;
612
+ }
613
+ }
614
+ // Note: We no longer extract wires from wireWorkflow - they come from pikkuWorkflowGraph
615
+ }
616
+ // If no graph property, this is a DSL workflow - skip (handled by add-workflow.ts)
617
+ if (!graphPropNode) {
618
+ return;
619
+ }
620
+ // Extract config from the pikkuWorkflowGraph result
621
+ const graphConfig = extractPikkuWorkflowGraphConfig(graphPropNode, checker);
622
+ if (!graphConfig) {
623
+ logger.critical(ErrorCode.MISSING_NAME, 'wireWorkflow with graph requires a pikkuWorkflowGraph');
624
+ return;
625
+ }
626
+ // Use explicit name or fall back to exported variable name
627
+ const workflowName = graphConfig.name || graphConfig.exportedName;
628
+ if (!workflowName) {
629
+ logger.critical(ErrorCode.MISSING_NAME, 'wireWorkflow with graph requires a pikkuWorkflowGraph with a name property or exported variable name');
630
+ return;
631
+ }
632
+ // Extract graph nodes from the new format (nodes + config properties)
633
+ let graphNodes = {};
634
+ if (graphConfig.nodesNode) {
635
+ graphNodes = extractGraphFromNewFormat(graphConfig.nodesNode, graphConfig.configNode, checker, state);
636
+ }
637
+ const entryNodeIds = computeEntryNodeIds(graphNodes);
638
+ // Use wires from pikkuWorkflowGraph (not from wireWorkflow)
639
+ const wires = graphConfig.wires || {};
640
+ const serialized = {
641
+ name: workflowName,
642
+ pikkuFuncName: workflowName,
643
+ source: 'graph',
644
+ description: graphConfig.description,
645
+ tags: graphConfig.tags,
646
+ wires,
647
+ nodes: graphNodes,
648
+ entryNodeIds,
649
+ };
650
+ // Only add if enabled (or not explicitly disabled)
651
+ if (enabled) {
652
+ state.workflows.graphMeta[workflowName] = serialized;
653
+ // Store file path and exported name for import generation
654
+ state.workflows.graphFiles.set(workflowName, {
655
+ path: node.getSourceFile().fileName,
656
+ exportedName: graphConfig.exportedName || workflowName,
657
+ });
658
+ }
659
+ };