ugcinc 4.0.2 → 4.1.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 (154) hide show
  1. package/dist/accounts.d.ts +146 -1
  2. package/dist/automations/index.d.ts +174 -0
  3. package/dist/automations/index.js +194 -0
  4. package/dist/automations/nodes/account.d.ts +18 -0
  5. package/dist/automations/nodes/account.js +29 -0
  6. package/dist/automations/nodes/auto-caption.d.ts +64 -0
  7. package/dist/automations/nodes/auto-caption.js +46 -0
  8. package/dist/automations/nodes/auto-post.d.ts +81 -0
  9. package/dist/automations/nodes/auto-post.js +54 -0
  10. package/dist/automations/nodes/branch.d.ts +58 -0
  11. package/dist/automations/nodes/branch.js +50 -0
  12. package/dist/automations/nodes/collect.d.ts +16 -0
  13. package/dist/automations/nodes/collect.js +56 -0
  14. package/dist/automations/nodes/compose-workflow.d.ts +21 -0
  15. package/dist/automations/nodes/compose-workflow.js +42 -0
  16. package/dist/automations/nodes/create-dm.d.ts +96 -0
  17. package/dist/automations/nodes/create-dm.js +88 -0
  18. package/dist/automations/nodes/custom-model.d.ts +19 -0
  19. package/dist/automations/nodes/custom-model.js +52 -0
  20. package/dist/automations/nodes/deduplicate.d.ts +8 -0
  21. package/dist/automations/nodes/deduplicate.js +36 -0
  22. package/dist/automations/nodes/destructure.d.ts +25 -0
  23. package/dist/automations/nodes/destructure.js +65 -0
  24. package/dist/automations/nodes/for-each.d.ts +23 -0
  25. package/dist/automations/nodes/for-each.js +84 -0
  26. package/dist/automations/nodes/generate-image.d.ts +16 -0
  27. package/dist/automations/nodes/generate-image.js +45 -0
  28. package/dist/automations/nodes/generate-video.d.ts +16 -0
  29. package/dist/automations/nodes/generate-video.js +45 -0
  30. package/dist/automations/nodes/if.d.ts +22 -0
  31. package/dist/automations/nodes/if.js +44 -0
  32. package/dist/automations/nodes/image-composer.d.ts +33 -0
  33. package/dist/automations/nodes/image-composer.js +95 -0
  34. package/dist/automations/nodes/index.d.ts +20 -0
  35. package/dist/automations/nodes/index.js +93 -0
  36. package/dist/automations/nodes/llm.d.ts +27 -0
  37. package/dist/automations/nodes/llm.js +85 -0
  38. package/dist/automations/nodes/manual-trigger.d.ts +34 -0
  39. package/dist/automations/nodes/manual-trigger.js +32 -0
  40. package/dist/automations/nodes/media.d.ts +17 -0
  41. package/dist/automations/nodes/media.js +40 -0
  42. package/dist/automations/nodes/not.d.ts +6 -0
  43. package/dist/automations/nodes/not.js +35 -0
  44. package/dist/automations/nodes/output.d.ts +30 -0
  45. package/dist/automations/nodes/output.js +32 -0
  46. package/dist/automations/nodes/random-route.d.ts +52 -0
  47. package/dist/automations/nodes/random-route.js +53 -0
  48. package/dist/automations/nodes/random.d.ts +16 -0
  49. package/dist/automations/nodes/random.js +50 -0
  50. package/dist/automations/nodes/recurrence.d.ts +69 -0
  51. package/dist/automations/nodes/recurrence.js +50 -0
  52. package/dist/automations/nodes/save-to-media.d.ts +32 -0
  53. package/dist/automations/nodes/save-to-media.js +31 -0
  54. package/dist/automations/nodes/screenshot-animation.d.ts +7 -0
  55. package/dist/automations/nodes/screenshot-animation.js +36 -0
  56. package/dist/automations/nodes/social-audio.d.ts +8 -0
  57. package/dist/automations/nodes/social-audio.js +30 -0
  58. package/dist/automations/nodes/text.d.ts +7 -0
  59. package/dist/automations/nodes/text.js +41 -0
  60. package/dist/automations/nodes/transcript.d.ts +6 -0
  61. package/dist/automations/nodes/transcript.js +46 -0
  62. package/dist/automations/nodes/types.d.ts +178 -0
  63. package/dist/automations/nodes/types.js +22 -0
  64. package/dist/automations/nodes/video-composer.d.ts +25 -0
  65. package/dist/automations/nodes/video-composer.js +71 -0
  66. package/dist/automations/nodes/video-import.d.ts +27 -0
  67. package/dist/automations/nodes/video-import.js +37 -0
  68. package/dist/automations/types.d.ts +544 -0
  69. package/dist/automations/types.js +101 -0
  70. package/dist/automations.d.ts +5 -33
  71. package/dist/automations.js +6 -647
  72. package/dist/comments.d.ts +26 -1
  73. package/dist/comments.js +3 -0
  74. package/dist/graph-controller.d.ts +194 -0
  75. package/dist/graph-controller.js +623 -0
  76. package/dist/index.d.ts +38 -9
  77. package/dist/index.js +43 -23
  78. package/dist/internal-utils.d.ts +8 -0
  79. package/dist/internal-utils.js +22 -0
  80. package/dist/media.d.ts +135 -1
  81. package/dist/nodes/account.d.ts +7 -0
  82. package/dist/nodes/account.js +29 -0
  83. package/dist/nodes/auto-caption.d.ts +17 -0
  84. package/dist/nodes/auto-caption.js +46 -0
  85. package/dist/nodes/auto-post.d.ts +21 -0
  86. package/dist/nodes/auto-post.js +54 -0
  87. package/dist/nodes/branch.d.ts +12 -0
  88. package/dist/nodes/branch.js +50 -0
  89. package/dist/nodes/collect.d.ts +6 -0
  90. package/dist/nodes/collect.js +56 -0
  91. package/dist/nodes/compose-workflow.d.ts +21 -0
  92. package/dist/nodes/compose-workflow.js +42 -0
  93. package/dist/nodes/create-dm.d.ts +40 -0
  94. package/dist/nodes/create-dm.js +88 -0
  95. package/dist/nodes/custom-model.d.ts +19 -0
  96. package/dist/nodes/custom-model.js +52 -0
  97. package/dist/nodes/deduplicate.d.ts +8 -0
  98. package/dist/nodes/deduplicate.js +36 -0
  99. package/dist/nodes/destructure.d.ts +25 -0
  100. package/dist/nodes/destructure.js +65 -0
  101. package/dist/nodes/for-each.d.ts +23 -0
  102. package/dist/nodes/for-each.js +84 -0
  103. package/dist/nodes/generate-image.d.ts +16 -0
  104. package/dist/nodes/generate-image.js +45 -0
  105. package/dist/nodes/generate-video.d.ts +16 -0
  106. package/dist/nodes/generate-video.js +45 -0
  107. package/dist/nodes/if.d.ts +22 -0
  108. package/dist/nodes/if.js +44 -0
  109. package/dist/nodes/image-composer.d.ts +14 -0
  110. package/dist/nodes/image-composer.js +95 -0
  111. package/dist/nodes/index.d.ts +20 -0
  112. package/dist/nodes/index.js +93 -0
  113. package/dist/nodes/llm.d.ts +27 -0
  114. package/dist/nodes/llm.js +85 -0
  115. package/dist/nodes/manual-trigger.d.ts +16 -0
  116. package/dist/nodes/manual-trigger.js +32 -0
  117. package/dist/nodes/media.d.ts +17 -0
  118. package/dist/nodes/media.js +40 -0
  119. package/dist/nodes/not.d.ts +6 -0
  120. package/dist/nodes/not.js +35 -0
  121. package/dist/nodes/output.d.ts +9 -0
  122. package/dist/nodes/output.js +32 -0
  123. package/dist/nodes/random-route.d.ts +3 -0
  124. package/dist/nodes/random-route.js +50 -0
  125. package/dist/nodes/random.d.ts +3 -0
  126. package/dist/nodes/random.js +48 -0
  127. package/dist/nodes/recurrence.d.ts +3 -0
  128. package/dist/nodes/recurrence.js +45 -0
  129. package/dist/nodes/save-to-media.d.ts +3 -0
  130. package/dist/nodes/save-to-media.js +26 -0
  131. package/dist/nodes/screenshot-animation.d.ts +7 -0
  132. package/dist/nodes/screenshot-animation.js +36 -0
  133. package/dist/nodes/social-audio.d.ts +3 -0
  134. package/dist/nodes/social-audio.js +26 -0
  135. package/dist/nodes/text.d.ts +3 -0
  136. package/dist/nodes/text.js +38 -0
  137. package/dist/nodes/transcript.d.ts +3 -0
  138. package/dist/nodes/transcript.js +42 -0
  139. package/dist/nodes/types.d.ts +146 -0
  140. package/dist/nodes/types.js +22 -0
  141. package/dist/nodes/video-composer.d.ts +3 -0
  142. package/dist/nodes/video-composer.js +67 -0
  143. package/dist/nodes/video-import.d.ts +3 -0
  144. package/dist/nodes/video-import.js +35 -0
  145. package/dist/org.d.ts +13 -1
  146. package/dist/ports.js +3 -9
  147. package/dist/posts.d.ts +88 -1
  148. package/dist/render/compositions/IMessageDmComposition/types.d.ts +20 -20
  149. package/dist/render/types/video.d.ts +2 -2
  150. package/dist/stats.d.ts +128 -1
  151. package/dist/tasks.d.ts +20 -1
  152. package/dist/types.d.ts +1 -2216
  153. package/dist/types.js +2 -124
  154. package/package.json +1 -1
@@ -0,0 +1,623 @@
1
+ "use strict";
2
+ /**
3
+ * Graph Controller
4
+ *
5
+ * Centralized logic for automation graph management including:
6
+ * - Port computation
7
+ * - Connection management
8
+ * - Type compatibility checking
9
+ * - Workflow validation
10
+ * - For-each context validation
11
+ *
12
+ * All functions work with WorkflowNodeDefinition[] (embedded connection format).
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.areTypesCompatible = areTypesCompatible;
16
+ exports.computePortsForNode = computePortsForNode;
17
+ exports.getAllNodes = getAllNodes;
18
+ exports.getNodeByType = getNodeByType;
19
+ exports.getOutputSchema = getOutputSchema;
20
+ exports.deriveConnections = deriveConnections;
21
+ exports.addConnection = addConnection;
22
+ exports.removeConnection = removeConnection;
23
+ exports.removeNodeConnections = removeNodeConnections;
24
+ exports.cleanupStaleConnections = cleanupStaleConnections;
25
+ exports.getForEachContext = getForEachContext;
26
+ exports.checkCrossContextViolation = checkCrossContextViolation;
27
+ exports.validateWorkflow = validateWorkflow;
28
+ exports.getErrorNodeIds = getErrorNodeIds;
29
+ exports.getPortErrorsForNode = getPortErrorsForNode;
30
+ exports.hasMissingTriggerError = hasMissingTriggerError;
31
+ exports.hasMissingTerminalError = hasMissingTerminalError;
32
+ const types_1 = require("./automations/types");
33
+ const nodes_1 = require("./automations/nodes");
34
+ // ===========================================================================
35
+ // Internal Helpers (not exported)
36
+ // ===========================================================================
37
+ /**
38
+ * Helper to check if a port type is a union (array of BasePortTypes)
39
+ */
40
+ function isTypeUnion(type) {
41
+ return Array.isArray(type);
42
+ }
43
+ /**
44
+ * Extract template variables from text (e.g., "Hello {{name}}" -> ["name"])
45
+ */
46
+ function extractTemplateVariables(texts) {
47
+ const variables = new Set();
48
+ const regex = /\{\{(\w+)\}\}/g;
49
+ for (const text of texts) {
50
+ let match;
51
+ while ((match = regex.exec(text)) !== null) {
52
+ if (match[1])
53
+ variables.add(match[1]);
54
+ }
55
+ }
56
+ return Array.from(variables);
57
+ }
58
+ /**
59
+ * Derive Connection[] array from nodes for internal processing
60
+ * Connections are stored in the embedded inputs map format.
61
+ */
62
+ function deriveConnectionsInternal(nodes) {
63
+ const connections = [];
64
+ nodes.forEach(node => {
65
+ if (!node.inputs)
66
+ return;
67
+ Object.entries(node.inputs).forEach(([inputId, mapping]) => {
68
+ connections.push({
69
+ id: `${mapping.sourceNodeId}-${mapping.sourceOutputId}-${node.id}-${inputId}`,
70
+ sourceNodeId: mapping.sourceNodeId,
71
+ sourceOutputId: mapping.sourceOutputId,
72
+ targetNodeId: node.id,
73
+ targetInputId: inputId,
74
+ });
75
+ });
76
+ });
77
+ return connections;
78
+ }
79
+ /**
80
+ * Get all nodes that are downstream of a for-each node (inside its context).
81
+ */
82
+ function getDownstreamNodes(forEachNodeId, nodes) {
83
+ const connections = deriveConnectionsInternal(nodes);
84
+ const downstream = new Set();
85
+ const queue = [];
86
+ // Find direct outputs from for-each
87
+ for (const conn of connections) {
88
+ if (conn.sourceNodeId === forEachNodeId) {
89
+ if (!downstream.has(conn.targetNodeId)) {
90
+ downstream.add(conn.targetNodeId);
91
+ queue.push(conn.targetNodeId);
92
+ }
93
+ }
94
+ }
95
+ // BFS to find all downstream
96
+ while (queue.length > 0) {
97
+ const currentId = queue.shift();
98
+ for (const conn of connections) {
99
+ if (conn.sourceNodeId === currentId && !downstream.has(conn.targetNodeId)) {
100
+ downstream.add(conn.targetNodeId);
101
+ queue.push(conn.targetNodeId);
102
+ }
103
+ }
104
+ }
105
+ return downstream;
106
+ }
107
+ /**
108
+ * Check if a node would be "captured" by a for-each context after a proposed connection.
109
+ *
110
+ * A node is captured if ALL of its outputs go to nodes inside the for-each context
111
+ * (either downstream nodes or other captured nodes).
112
+ */
113
+ function wouldBeCaptured(nodeId, forEachNodeId, downstreamNodes, nodes, proposedTargetNodeId) {
114
+ const node = nodes.find(n => n.id === nodeId);
115
+ if (!node)
116
+ return false;
117
+ // For-each nodes can't be captured
118
+ if (node.type === 'for-each')
119
+ return false;
120
+ // Already downstream = not captured (it's inside)
121
+ if (downstreamNodes.has(nodeId))
122
+ return false;
123
+ const connections = deriveConnectionsInternal(nodes);
124
+ // Get all targets this node outputs to (existing + proposed)
125
+ const outputTargets = new Set();
126
+ for (const conn of connections) {
127
+ if (conn.sourceNodeId === nodeId) {
128
+ outputTargets.add(conn.targetNodeId);
129
+ }
130
+ }
131
+ // Add the proposed connection target
132
+ outputTargets.add(proposedTargetNodeId);
133
+ // If no outputs, can't be captured
134
+ if (outputTargets.size === 0)
135
+ return false;
136
+ // Build captured set iteratively
137
+ const captured = new Set();
138
+ let changed = true;
139
+ while (changed) {
140
+ changed = false;
141
+ for (const n of nodes) {
142
+ if (n.id === forEachNodeId)
143
+ continue;
144
+ if (downstreamNodes.has(n.id))
145
+ continue;
146
+ if (captured.has(n.id))
147
+ continue;
148
+ // Get outputs for this node
149
+ const nodeOutputs = new Set();
150
+ for (const conn of connections) {
151
+ if (conn.sourceNodeId === n.id) {
152
+ nodeOutputs.add(conn.targetNodeId);
153
+ }
154
+ }
155
+ // Include proposed connection if this is the source node
156
+ if (n.id === nodeId) {
157
+ nodeOutputs.add(proposedTargetNodeId);
158
+ }
159
+ if (nodeOutputs.size === 0)
160
+ continue;
161
+ // All outputs must go to downstream or captured
162
+ const allInside = [...nodeOutputs].every(targetId => downstreamNodes.has(targetId) || captured.has(targetId));
163
+ if (allInside) {
164
+ captured.add(n.id);
165
+ changed = true;
166
+ }
167
+ }
168
+ }
169
+ return captured.has(nodeId);
170
+ }
171
+ /**
172
+ * Find the for-each context a node belongs to by tracing backwards.
173
+ *
174
+ * A node is "inside" a for-each context if, when tracing backwards through
175
+ * connections, we reach a for-each node via one of its OUTPUT ports.
176
+ *
177
+ * Returns the for-each node ID if inside a context, or null if outside all contexts.
178
+ */
179
+ function getForEachContextInternal(nodeId, nodes, connections, visited = new Set()) {
180
+ // Prevent infinite loops from circular references
181
+ if (visited.has(nodeId))
182
+ return null;
183
+ visited.add(nodeId);
184
+ const node = nodes.find(n => n.id === nodeId);
185
+ if (!node)
186
+ return null;
187
+ // Check all inputs to this node
188
+ for (const connection of connections) {
189
+ if (connection.targetNodeId !== nodeId)
190
+ continue;
191
+ const sourceNode = nodes.find(n => n.id === connection.sourceNodeId);
192
+ if (!sourceNode)
193
+ continue;
194
+ // If the source is a for-each node and we're connected to one of its outputs,
195
+ // then this node is inside that for-each context
196
+ if (sourceNode.type === 'for-each') {
197
+ return sourceNode.id;
198
+ }
199
+ // Otherwise, recursively check the source node's context
200
+ const sourceContext = getForEachContextInternal(connection.sourceNodeId, nodes, connections, visited);
201
+ if (sourceContext) {
202
+ return sourceContext;
203
+ }
204
+ }
205
+ return null;
206
+ }
207
+ // ===========================================================================
208
+ // Type Compatibility
209
+ // ===========================================================================
210
+ /**
211
+ * Check if two port types are compatible for connection
212
+ * A connection is valid if:
213
+ * 1. The types match exactly (same base type and same isArray), OR
214
+ * 2. The source is an array type and target is 'object' (non-array), OR
215
+ * 3. The source is a union and includes a compatible type, OR
216
+ * 4. The target is a union and includes a compatible type, OR
217
+ * 5. Both are unions and have at least one common compatible type
218
+ *
219
+ * Note: Array inputs are strict - they only accept exact matches
220
+ */
221
+ function areTypesCompatible({ sourceType, sourceIsArray, targetType, targetIsArray, }) {
222
+ const sourceTypes = isTypeUnion(sourceType) ? sourceType : [sourceType];
223
+ const targetTypes = isTypeUnion(targetType) ? targetType : [targetType];
224
+ return sourceTypes.some(st => {
225
+ return targetTypes.some(tt => {
226
+ // Exact match (same base type and same isArray)
227
+ if (st === tt && sourceIsArray === targetIsArray)
228
+ return true;
229
+ // Object inputs (non-array) accept any array type (backwards compatibility with for-each, etc.)
230
+ if (tt === 'object' && !targetIsArray && sourceIsArray) {
231
+ return true;
232
+ }
233
+ // Array inputs are strict - no other compatibility
234
+ if (targetIsArray) {
235
+ return false;
236
+ }
237
+ return false;
238
+ });
239
+ });
240
+ }
241
+ // ===========================================================================
242
+ // Port Computation
243
+ // ===========================================================================
244
+ /**
245
+ * Compute ports for a node based on its type and config.
246
+ * Uses the node definition's computePorts function.
247
+ *
248
+ * @param nodeType - The type of the node
249
+ * @param config - The node's configuration object
250
+ * @param getConnectedOutput - Optional function to get connected output port info (for dynamic ports)
251
+ */
252
+ function computePortsForNode({ nodeType, config, getConnectedOutput, }) {
253
+ const definition = (0, nodes_1.getNodeDefinition)(nodeType);
254
+ if (!definition) {
255
+ return { inputs: [], outputs: [] };
256
+ }
257
+ return definition.computePorts({
258
+ config: config ?? definition.defaults,
259
+ getConnectedOutput,
260
+ });
261
+ }
262
+ /**
263
+ * Get all available automation nodes.
264
+ * Converts node definitions from the registry into NodeControlConfig format.
265
+ */
266
+ function getAllNodes() {
267
+ return (0, nodes_1.getAllNodeDefinitions)().map(def => {
268
+ const { inputs, outputs } = def.computePorts({ config: def.defaults });
269
+ return {
270
+ nodeId: def.nodeId,
271
+ label: def.label,
272
+ description: def.description,
273
+ category: def.category,
274
+ type: def.type,
275
+ inputs,
276
+ outputs,
277
+ outputModes: def.outputModes,
278
+ selectionModes: def.selectionModes,
279
+ defaults: def.defaults,
280
+ };
281
+ });
282
+ }
283
+ /**
284
+ * Get a specific node by nodeId
285
+ */
286
+ function getNodeByType(nodeId) {
287
+ return getAllNodes().find(node => node.nodeId === nodeId);
288
+ }
289
+ // ===========================================================================
290
+ // Output Schema
291
+ // ===========================================================================
292
+ /**
293
+ * Get the schema of array items for a node's output port.
294
+ * Returns null if schema is unknown or the output is not an array.
295
+ *
296
+ * @param nodeType - The type of the node (e.g., 'transcript', 'llm')
297
+ * @param nodeConfig - The node's configuration object
298
+ * @param outputPortId - The ID of the output port to get schema for
299
+ * @returns The schema of array items, or null if unknown
300
+ */
301
+ function getOutputSchema({ nodeType, nodeConfig, outputPortId, }) {
302
+ // First check static node definitions
303
+ const nodeDefinition = getNodeByType(nodeType);
304
+ const outputPort = nodeDefinition?.outputs.find(o => o.id === outputPortId);
305
+ if (outputPort?.objectSchema) {
306
+ return outputPort.objectSchema;
307
+ }
308
+ // Check resolvedPorts for nodes with dynamic outputs (compose-workflow, for-each, etc.)
309
+ if (nodeConfig?.resolvedPorts) {
310
+ const resolvedPorts = nodeConfig.resolvedPorts;
311
+ const resolvedOutput = resolvedPorts.outputs?.find(o => o.id === outputPortId);
312
+ if (resolvedOutput?.objectSchema) {
313
+ return resolvedOutput.objectSchema;
314
+ }
315
+ }
316
+ return null;
317
+ }
318
+ // ===========================================================================
319
+ // Connection Management
320
+ // ===========================================================================
321
+ /**
322
+ * Derive Connection[] array from nodes for canvas rendering
323
+ *
324
+ * Connections are stored in the embedded inputs map format.
325
+ * This function extracts them into a flat array for canvas rendering.
326
+ */
327
+ function deriveConnections({ nodes }) {
328
+ return deriveConnectionsInternal(nodes);
329
+ }
330
+ /**
331
+ * Add a connection to a node's inputs map
332
+ */
333
+ function addConnection({ nodes, sourceNodeId, sourceOutputId, targetNodeId, targetInputId, }) {
334
+ return nodes.map(node => {
335
+ if (node.id !== targetNodeId)
336
+ return node;
337
+ return {
338
+ ...node,
339
+ inputs: {
340
+ ...node.inputs,
341
+ [targetInputId]: {
342
+ sourceNodeId,
343
+ sourceOutputId,
344
+ },
345
+ },
346
+ };
347
+ });
348
+ }
349
+ /**
350
+ * Remove a connection from a node's inputs map
351
+ */
352
+ function removeConnection({ nodes, targetNodeId, targetInputId, }) {
353
+ return nodes.map(node => {
354
+ if (node.id !== targetNodeId)
355
+ return node;
356
+ const newInputs = { ...node.inputs };
357
+ delete newInputs[targetInputId];
358
+ return {
359
+ ...node,
360
+ inputs: newInputs,
361
+ };
362
+ });
363
+ }
364
+ /**
365
+ * Remove all connections to/from a node
366
+ */
367
+ function removeNodeConnections({ nodes, nodeId, }) {
368
+ return nodes.map(node => {
369
+ // Remove inputs that come from the deleted node
370
+ const newInputs = { ...node.inputs };
371
+ let hasChanges = false;
372
+ Object.entries(newInputs).forEach(([inputId, mapping]) => {
373
+ if (mapping.sourceNodeId === nodeId) {
374
+ delete newInputs[inputId];
375
+ hasChanges = true;
376
+ }
377
+ });
378
+ if (hasChanges) {
379
+ return { ...node, inputs: newInputs };
380
+ }
381
+ return node;
382
+ });
383
+ }
384
+ /**
385
+ * Clean up stale connections on load.
386
+ *
387
+ * Removes entries from node.inputs that reference ports that no longer exist
388
+ * in the node's resolvedPorts.inputs. This handles cases where ports were
389
+ * renamed or deleted but the old connection data persisted.
390
+ */
391
+ function cleanupStaleConnections({ nodes, }) {
392
+ return nodes.map(node => {
393
+ const resolvedInputs = node.config?.resolvedPorts?.inputs;
394
+ // If no resolvedPorts.inputs, we can't validate - skip cleanup for this node
395
+ if (!resolvedInputs || !Array.isArray(resolvedInputs)) {
396
+ return node;
397
+ }
398
+ const validInputIds = new Set(resolvedInputs.map(p => p.id));
399
+ const currentInputs = node.inputs ?? {};
400
+ // Check if any inputs are stale (reference non-existent ports)
401
+ const staleInputIds = Object.keys(currentInputs).filter(inputId => !validInputIds.has(inputId));
402
+ if (staleInputIds.length === 0) {
403
+ return node; // No cleanup needed
404
+ }
405
+ // Remove stale inputs
406
+ const cleanedInputs = { ...currentInputs };
407
+ staleInputIds.forEach(inputId => {
408
+ delete cleanedInputs[inputId];
409
+ });
410
+ return {
411
+ ...node,
412
+ inputs: cleanedInputs,
413
+ };
414
+ });
415
+ }
416
+ // ===========================================================================
417
+ // For-Each Context Validation
418
+ // ===========================================================================
419
+ /**
420
+ * Find the for-each context a node belongs to by tracing backwards.
421
+ *
422
+ * A node is "inside" a for-each context if, when tracing backwards through
423
+ * connections, we reach a for-each node via one of its OUTPUT ports.
424
+ *
425
+ * Returns the for-each node ID if inside a context, or null if outside all contexts.
426
+ */
427
+ function getForEachContext({ nodeId, nodes, }) {
428
+ const connections = deriveConnectionsInternal(nodes);
429
+ return getForEachContextInternal(nodeId, nodes, connections);
430
+ }
431
+ /**
432
+ * Check if a connection would violate for-each context boundaries.
433
+ *
434
+ * A violation occurs when:
435
+ * 1. Source is OUTSIDE a for-each context and target is INSIDE,
436
+ * AND the source would NOT become a "captured" node after this connection
437
+ * 2. Source and target are in DIFFERENT for-each contexts
438
+ * (can't connect across different loops)
439
+ *
440
+ * "Captured" nodes are allowed - they will be expanded at runtime to execute
441
+ * once per for-each iteration (e.g., Account selector → Auto Post inside for-each).
442
+ *
443
+ * Returns true if the connection is INVALID (violation), false if allowed.
444
+ */
445
+ function checkCrossContextViolation({ sourceNodeId, targetNodeId, nodes, }) {
446
+ const connections = deriveConnectionsInternal(nodes);
447
+ const targetNode = nodes.find(n => n.id === targetNodeId);
448
+ if (!targetNode)
449
+ return false;
450
+ // If target is a for-each node, the connection is going INTO the for-each
451
+ // (to its array input or passthrough inputs), which is always allowed
452
+ if (targetNode.type === 'for-each') {
453
+ return false;
454
+ }
455
+ const sourceNode = nodes.find(n => n.id === sourceNodeId);
456
+ const targetContext = getForEachContextInternal(targetNodeId, nodes, connections);
457
+ // If source IS the for-each node that defines the target's context,
458
+ // allow the connection (for-each feeding its own loop body via passthrough outputs)
459
+ if (sourceNode?.type === 'for-each' && targetContext === sourceNodeId) {
460
+ return false;
461
+ }
462
+ const sourceContext = getForEachContextInternal(sourceNodeId, nodes, connections);
463
+ // If source is outside and target is inside a for-each context...
464
+ if (sourceContext === null && targetContext !== null) {
465
+ // Check if source would become a "captured" node after this connection
466
+ const downstreamNodes = getDownstreamNodes(targetContext, nodes);
467
+ if (wouldBeCaptured(sourceNodeId, targetContext, downstreamNodes, nodes, targetNodeId)) {
468
+ // Source will be captured - allow the connection
469
+ return false;
470
+ }
471
+ // Source has other outputs outside the context - violation
472
+ return true;
473
+ }
474
+ // If both are inside different for-each contexts, that's a violation
475
+ if (sourceContext !== null && targetContext !== null && sourceContext !== targetContext) {
476
+ return true;
477
+ }
478
+ return false;
479
+ }
480
+ // ===========================================================================
481
+ // Workflow Validation
482
+ // ===========================================================================
483
+ const TRIGGER_TYPES = ['manual-trigger', 'recurrence'];
484
+ const TERMINAL_TYPES = ['passthrough', 'auto-post', 'save-to-media'];
485
+ /**
486
+ * Validate workflow and return structured errors for UI highlighting
487
+ */
488
+ function validateWorkflow({ nodes, }) {
489
+ const errors = [];
490
+ const connections = deriveConnectionsInternal(nodes);
491
+ // 1. Check for trigger node
492
+ const hasTrigger = nodes.some(n => TRIGGER_TYPES.includes(n.type));
493
+ if (!hasTrigger) {
494
+ errors.push({
495
+ type: 'missing_trigger',
496
+ message: 'Automation requires a trigger node (Manual Trigger or Recurrence)',
497
+ });
498
+ }
499
+ // 2. Check for terminal node
500
+ const hasTerminal = nodes.some(n => TERMINAL_TYPES.includes(n.type));
501
+ if (!hasTerminal) {
502
+ errors.push({
503
+ type: 'missing_terminal',
504
+ message: 'Automation requires an output node (Output, Auto Post, or Save to Media)',
505
+ });
506
+ }
507
+ // Build connection lookup: targetNodeId-targetInputId -> connection
508
+ const connectionsByTarget = new Map();
509
+ connections.forEach(conn => {
510
+ connectionsByTarget.set(`${conn.targetNodeId}-${conn.targetInputId}`, conn);
511
+ });
512
+ // Build node lookup by ID
513
+ const nodeById = new Map();
514
+ nodes.forEach(n => nodeById.set(n.id, n));
515
+ // 3. Check each node for missing required inputs and type mismatches
516
+ for (const node of nodes) {
517
+ // Use node definition's computePorts if available
518
+ const definition = (0, nodes_1.getNodeDefinition)(node.type);
519
+ const inputPorts = definition
520
+ ? definition.computePorts({ config: node.config ?? {} }).inputs
521
+ : [];
522
+ for (const port of inputPorts) {
523
+ if (!port.required)
524
+ continue;
525
+ const connectionKey = `${node.id}-${port.id}`;
526
+ const connection = connectionsByTarget.get(connectionKey);
527
+ if (!connection) {
528
+ // Missing required input
529
+ errors.push({
530
+ type: 'missing_required_input',
531
+ nodeId: node.id,
532
+ portId: port.id,
533
+ message: `Required input '${port.id}' is not connected`,
534
+ });
535
+ }
536
+ else {
537
+ // Check type compatibility
538
+ const sourceNode = nodeById.get(connection.sourceNodeId);
539
+ if (sourceNode) {
540
+ const sourceDefinition = (0, nodes_1.getNodeDefinition)(sourceNode.type);
541
+ const sourceOutputs = sourceDefinition
542
+ ? sourceDefinition.computePorts({ config: sourceNode.config ?? {} }).outputs
543
+ : [];
544
+ const sourcePort = sourceOutputs.find(p => p.id === connection.sourceOutputId);
545
+ if (sourcePort) {
546
+ const compatible = areTypesCompatible({
547
+ sourceType: sourcePort.type,
548
+ sourceIsArray: sourcePort.isArray,
549
+ targetType: port.type,
550
+ targetIsArray: port.isArray,
551
+ });
552
+ if (!compatible) {
553
+ errors.push({
554
+ type: 'type_mismatch',
555
+ nodeId: node.id,
556
+ portId: port.id,
557
+ message: `Type mismatch: expected ${formatPortType(port.type, port.isArray)}, got ${formatPortType(sourcePort.type, sourcePort.isArray)}`,
558
+ });
559
+ }
560
+ }
561
+ }
562
+ }
563
+ }
564
+ // 4. Check for empty media pool
565
+ if (node.type === 'media') {
566
+ const configOutputs = (node.config?.outputs ?? []);
567
+ // Check if ANY output has media selected
568
+ const totalSelected = configOutputs.reduce((sum, o) => sum + o.selectedMediaIds.length, 0);
569
+ if (totalSelected === 0) {
570
+ errors.push({
571
+ type: 'empty_media_pool',
572
+ nodeId: node.id,
573
+ message: 'Media node has no media selected',
574
+ });
575
+ }
576
+ }
577
+ // 5. Check for empty account pool
578
+ if (node.type === 'account') {
579
+ const accountConfig = node.config?.accountConfig;
580
+ const accountIds = accountConfig?.accountIds ?? [];
581
+ if (accountIds.length === 0) {
582
+ errors.push({
583
+ type: 'empty_account_pool',
584
+ nodeId: node.id,
585
+ message: 'Account node has no accounts selected',
586
+ });
587
+ }
588
+ }
589
+ }
590
+ return errors;
591
+ }
592
+ /**
593
+ * Format a port type for display in error messages
594
+ */
595
+ function formatPortType(type, isArray) {
596
+ return (0, types_1.formatPortType)(type, isArray);
597
+ }
598
+ /**
599
+ * Get node IDs that have errors
600
+ */
601
+ function getErrorNodeIds({ errors }) {
602
+ return [...new Set(errors.filter(e => e.nodeId).map(e => e.nodeId))];
603
+ }
604
+ /**
605
+ * Get port errors for a specific node
606
+ */
607
+ function getPortErrorsForNode({ nodeId, errors, }) {
608
+ return errors
609
+ .filter(e => e.nodeId === nodeId && e.portId)
610
+ .map(e => ({ portId: e.portId, message: e.message }));
611
+ }
612
+ /**
613
+ * Check if there's a missing trigger error
614
+ */
615
+ function hasMissingTriggerError({ errors }) {
616
+ return errors.some(e => e.type === 'missing_trigger');
617
+ }
618
+ /**
619
+ * Check if there's a missing terminal error
620
+ */
621
+ function hasMissingTerminalError({ errors }) {
622
+ return errors.some(e => e.type === 'missing_terminal');
623
+ }
package/dist/index.d.ts CHANGED
@@ -10,15 +10,44 @@ export { PostsClient } from './posts';
10
10
  export { StatsClient } from './stats';
11
11
  export { OrganizationClient } from './org';
12
12
  export { RenderClient } from './render';
13
- export { AutomationsClient, getAllNodes, getNodeByType, getOutputSchema, areTypesCompatible } from './automations';
14
- export { computeInputPorts, computeOutputPorts, isInputPortRequired, extractTemplateVariables } from './ports';
15
- export type { ComputedPort } from './ports';
16
- export { NodeTypes, isAsyncExecutor, isEditModel, isImageToVideoModel, portType, formatPortType, isLegacyPortType, fromLegacyPortType, toLegacyPortType, normalizePortType, normalizePortTypes } from './types';
17
- export type { PropertySchema } from './automations';
18
- export { portId, isValidPortId, portIdToTitle, normalizeToPortId, PortIdSchema } from './port-id';
19
- export type { PortId } from './port-id';
13
+ export { AutomationsClient } from './automations';
20
14
  export { MediaClient } from './media';
21
15
  export { CommentsClient } from './comments';
16
+ export { areTypesCompatible, computePortsForNode, getAllNodes, getNodeByType, getOutputSchema, deriveConnections, addConnection, removeConnection, removeNodeConnections, cleanupStaleConnections, getForEachContext, checkCrossContextViolation, validateWorkflow, getErrorNodeIds, getPortErrorsForNode, hasMissingTriggerError, hasMissingTerminalError, } from './graph-controller';
17
+ export { NodeTypes, isAsyncExecutor, formatPortType } from './automations/types';
18
+ export { isEditModel } from './automations/nodes/generate-image';
19
+ export { isImageToVideoModel } from './automations/nodes/generate-video';
20
+ export { portId, isValidPortId, portIdToTitle, normalizeToPortId, PortIdSchema } from './port-id';
21
+ export type { PortId } from './port-id';
22
+ export type { ClientConfig } from './base';
23
+ export type { Account, AccountStat, AccountTask, EditProfileInfo, GetAccountsParams, GetAccountStatsParams, GetAccountStatusParams, AccountInfoUpdate, UpdateAccountInfoParams, AccountInfoUpdateResult, UpdateAccountInfoResponse, AccountSocialUpdate, UpdateAccountSocialParams, AccountSocialUpdateResult, UpdateAccountSocialResponse, DeleteAccountPostsParams, DeleteAccountPostsResponse, ResetWarmupParams, ResetWarmupResponse, } from './accounts';
24
+ export type { TaskType, Task, GetTasksParams } from './tasks';
25
+ export type { PostType, PostStatus, Post, PostStat, GetPostsParams, CreateSlideshowParams, GetPostStatsParams, GetPostStatusParams, CreateVideoParams, DeletePostsParams, DeletePostsResponse, RetryPostsParams, SetPostStatusParams, SetPostStatusResponse, } from './posts';
26
+ export type { RefreshStatsParams, RefreshStatsError, RefreshStatsResponse, DailyAggregatedStat, GetDailyAggregatedStatsParams, DailyAccountStat, GetDailyAccountStatsParams, DailyPostStat, GetDailyPostStatsParams, TopAccount, GetTopAccountsParams, TopPost, GetTopPostsParams, } from './stats';
27
+ export type { ApiKey, DeleteApiKeyParams, EditApiKeyParams } from './org';
28
+ export type { UserMedia, MediaUse, SocialAudio, Media, GetMediaParams, GetSocialAudioParams, UploadMediaParams, UploadMediaResponse, MediaTagUpdate, UpdateMediaTagsParams, MediaTagUpdateResult, UpdateMediaTagsResponse, UpdateMediaTagParams, DeleteMediaParams, DeleteMediaResponse, CreateSocialAudioParams, ImportTextParams, ImportTextResponse, CreateMediaFromUrlParams, GetMediaUseParams, GetMediaUseResponse, FilterMediaParams, FilterMediaResponse, GetUploadTokenParams, UploadTokenResponse, } from './media';
29
+ export type { CommentStatus, Comment, CreateCommentParams, CreateCommentResponse, GetCommentsParams, } from './comments';
22
30
  export type { RenderJobResponse, RenderJobStatus, SubmitImageRenderJobParams, SubmitVideoRenderJobParams, SubmitScreenshotAnimationRenderJobParams, SubmitInstagramDmRenderJobParams, SubmitIMessageDmRenderJobParams, IgDmMessage, ImDmMessage, RenderVideoEditorConfig, } from './render';
23
- export type { ClientConfig, } from './base';
24
- export type { SuccessResponse, ErrorResponse, ApiResponse, Account, AccountStat, AccountTask, EditProfileInfo, Task, TaskType, Post, PostType, PostStatus, PostStat, ApiKey, EditorConfig, VideoEditorNodeConfig, VideoEditorChannel, VideoEditorSegment, VideoEditorVideoSegment, VideoEditorAudioSegment, VideoEditorImageSegment, VideoEditorTextSegment, TimeMode, SegmentTimelinePosition, ImageEditorNodeConfig, ImageEditorElement, ImageEditorNodeInput, ImageEditorNodeOutput, VideoEditorNodeInput, DimensionPresetKey, EditorChannel, TimeValue, DeduplicationInput, BaseSegmentProps, VisualSegmentProps, EditorSegment, VideoSegment, AudioSegment, ImageSegment, TextSegment, StaticSegment, PositionAnchor, RelativePositionConfig, GetAccountsParams, GetAccountStatsParams, GetAccountStatusParams, UpdateAccountInfoParams, UpdateAccountSocialParams, GetTasksParams, GetPostsParams, CreateSlideshowParams, GetPostStatsParams, GetPostStatusParams, CreateVideoParams, RefreshStatsParams, RefreshStatsResponse, RefreshStatsError, DailyAggregatedStat, GetDailyAggregatedStatsParams, DailyPostStat, GetDailyPostStatsParams, TopAccount, GetTopAccountsParams, TopPost, GetTopPostsParams, MediaType, NodePort, ResolvedPort, ResolvedPorts, NodeControlConfig, NodeTypeEnum, WorkflowNodeDefinition, WorkflowDefinition, CanvasState, TemplateNode, AutomationTemplate, AutomationRun, NodeRun, ExecutorNode, ExecutionEdge, OutputSchemaProperty, SelectionMode, ExhaustionBehavior, SelectionConfig, SelectionState, OutputMode, NodeOutputValues, OutputInput, OutputNodeConfig, ObjectSchemaFieldLevel3, ObjectSchemaFieldLevel2, ObjectSchemaField, ManualTriggerOutput, ManualTriggerNodeConfig, TriggerIterationMode, IterationExhaustionBehavior, CollectionSelectionMode, TriggerCollectionConfig, DayOfWeek, RecurrenceMediaOutput, RecurrenceMediaConfig, RecurrenceNodeConfig, AccountNodeConfig, MediaNodeEnabledType, RecurrenceMediaEnabledType, MediaNodeOutput, MediaNodeConfig, PostSchedulingMode, PostSchedulingConfig, AutoPostMode, AutoPostInputType, AutoPostInput, AutoPostNodeConfig, SaveToMediaInput, SaveToMediaNodeConfig, DeduplicateNodeConfig, ForEachOutputProperty, ForEachInputPort, ForEachNodeConfig, RandomValueType, RandomInputPort, RandomNodeMode, RandomNodeConfig, IfLogicOperator, IfBooleanInput, IfPassthroughInput, IfNodeConfig, RandomRouteBranch, RandomRoutePassthroughInput, RandomRouteObjectField, RandomRouteNodeConfig, BranchDefinition, BranchValueConfig, BranchPassthroughInput, BranchNodeConfig, VideoImportPlatform, VideoImportQuality, VideoImportNodeConfig, AutoCaptionPreset, AutoCaptionFontWeight, AutoCaptionPosition, AutoCaptionNodeConfig, ScreenshotAnimationNodeConfig, CreateDmMessage, CreateDmNodeConfig, DestructureSelection, DestructureNodeConfig, ImageGenerationTextModel, ImageGenerationEditModel, ImageGenerationModel, ImageGenerationNodeConfig, CustomModelParamType, CustomModelInputParam, CustomModelNodeConfig, VideoGenerationTextToVideoModel, VideoGenerationImageToVideoModel, VideoGenerationModel, VideoGenerationNodeConfig, BasePortType, PortType, LegacyPortType, LLMOutputField, LLMApiKeys, LLMNodeConfig, UserMedia, SocialAudio, Media, MediaUse, GetMediaParams, CreateSocialAudioParams, UploadMediaParams, UploadMediaResponse, UpdateMediaTagParams, DeleteMediaParams, DeleteMediaResponse, CreateMediaFromUrlParams, ImportTextParams, ImportTextResponse, GetMediaUseParams, GetMediaUseResponse, FilterMediaParams, FilterMediaResponse, Comment, CommentStatus, CreateCommentParams, CreateCommentResponse, GetCommentsParams, ValidationErrorType, ValidationError, ExportedNode, ExportedConnection, ExportedTemplate, AutomationExport, ExportedExecutor, ExportedEdge, ExportedRun, AutomationRunExport, AccountData, FlowControlOutput, NodeType, ExecutorContext, NodeExecutor, ExecutorAsyncJobStatus, AsyncNodeExecutor, } from './types';
31
+ export type { VideoEditorNodeConfig, VideoEditorChannel, VideoEditorSegment, VideoEditorVideoSegment, VideoEditorAudioSegment, VideoEditorImageSegment, VideoEditorTextSegment, VideoEditorImageSequenceSegment, VideoEditorVideoSequenceSegment, TimeValue, TimeMode, SegmentTimelinePosition, DeduplicationInput, ImageEditorElement, ImageEditorNodeConfig, DimensionPresetKey, } from './render/types';
32
+ export type { MediaType, BasePortType, EnumOption, SelectionMode, ExhaustionBehavior, SelectionConfig, SelectionState, OutputMode, NodeOutputValues, NodePort, ResolvedPort, ResolvedPorts, NodeCategory, NodeControlConfig, NodeDefinition, NodeTypeEnum, NodeType, UserCreatableNodeType, OutputSchemaProperty, WorkflowNodeDefinition, CanvasState, WorkflowDefinition, TemplateNode, AutomationTemplate, AutomationRun, ExecutorNode, ExecutionEdge, AccountData, FlowControlOutput, ExecutorContext, NodeExecutor, AsyncNodeExecutor, ExecutorAsyncJobStatus, ValidationErrorType, ValidationError, ExportedNode, ExportedConnection, ExportedTemplate, AutomationExport, ExportedExecutor, ExportedEdge, ExportedRun, AutomationRunExport, } from './automations/types';
33
+ export type { MediaNodeOutput, MediaNodeEnabledType, MediaConfig } from './automations/nodes/media';
34
+ export type { ScreenshotAnimationConfig } from './automations/nodes/screenshot-animation';
35
+ /** @deprecated Use ScreenshotAnimationConfig instead */
36
+ export type { ScreenshotAnimationConfig as ScreenshotAnimationNodeConfig } from './automations/nodes/screenshot-animation';
37
+ export type { RandomRouteBranch, RandomRouteObjectField, RandomRoutePassthroughInput, RandomRouteNodeConfig, } from './automations/nodes/random-route';
38
+ export type { BranchDefinition, BranchValueConfig, BranchPassthroughInput, BranchNodeConfig, } from './automations/nodes/branch';
39
+ export type { VideoImportPlatform, VideoImportQuality, VideoImportNodeConfig, } from './automations/nodes/video-import';
40
+ export type { AutoCaptionPreset, AutoCaptionFontWeight, AutoCaptionPosition, AutoCaptionNodeConfig, } from './automations/nodes/auto-caption';
41
+ export type { CreateDmMessage, CreateDmNodeConfig, } from './automations/nodes/create-dm';
42
+ export type { CollectNodeConfig } from './automations/nodes/collect';
43
+ export type { OutputInput, OutputNodeConfig, } from './automations/nodes/output';
44
+ export type { ManualTriggerOutput, ManualTriggerNodeConfig, } from './automations/nodes/manual-trigger';
45
+ export type { DayOfWeek, RecurrenceMediaOutput, RecurrenceMediaConfig, RecurrenceNodeConfig, RecurrenceMediaEnabledType, } from './automations/nodes/recurrence';
46
+ export type { AccountNodeConfig } from './automations/nodes/account';
47
+ export type { PostSchedulingMode, PostSchedulingConfig, AutoPostMode, AutoPostInputType, AutoPostInput, AutoPostNodeConfig, } from './automations/nodes/auto-post';
48
+ export type { SaveToMediaInput, SaveToMediaNodeConfig, } from './automations/nodes/save-to-media';
49
+ export type { TriggerIterationMode, IterationExhaustionBehavior, CollectionSelectionMode, TriggerCollectionConfig, ObjectSchemaField, } from './automations/nodes/types';
50
+ export type { ImageComposerConfig, ImageEditorNodeInput, ImageEditorNodeOutput, } from './automations/nodes/image-composer';
51
+ export type { VideoComposerConfig, VideoEditorNodeInput, } from './automations/nodes/video-composer';
52
+ export type { SuccessResponse, ErrorResponse, ApiResponse, } from './types';
53
+ export type { CropBoundary, CropAxisConfig, DynamicCropConfig, BorderRadiusConfig, VerticalAnchor, HorizontalAnchor, HorizontalSelfAnchor, VerticalSelfAnchor, RelativePositionConfigX, RelativePositionConfigY, } from './render/types';