@elastic.io/mcp 1.0.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 (155) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +171 -0
  3. package/dist/api/client.d.ts +12 -0
  4. package/dist/api/client.d.ts.map +1 -0
  5. package/dist/api/client.js +124 -0
  6. package/dist/api/client.js.map +1 -0
  7. package/dist/api/jsonapi.d.ts +28 -0
  8. package/dist/api/jsonapi.d.ts.map +1 -0
  9. package/dist/api/jsonapi.js +45 -0
  10. package/dist/api/jsonapi.js.map +1 -0
  11. package/dist/api/logger.d.ts +9 -0
  12. package/dist/api/logger.d.ts.map +1 -0
  13. package/dist/api/logger.js +46 -0
  14. package/dist/api/logger.js.map +1 -0
  15. package/dist/api/samples.d.ts +21 -0
  16. package/dist/api/samples.d.ts.map +1 -0
  17. package/dist/api/samples.js +465 -0
  18. package/dist/api/samples.js.map +1 -0
  19. package/dist/config.d.ts +16 -0
  20. package/dist/config.d.ts.map +1 -0
  21. package/dist/config.js +49 -0
  22. package/dist/config.js.map +1 -0
  23. package/dist/embeddings/factory.d.ts +7 -0
  24. package/dist/embeddings/factory.d.ts.map +1 -0
  25. package/dist/embeddings/factory.js +25 -0
  26. package/dist/embeddings/factory.js.map +1 -0
  27. package/dist/embeddings/google.d.ts +19 -0
  28. package/dist/embeddings/google.d.ts.map +1 -0
  29. package/dist/embeddings/google.js +132 -0
  30. package/dist/embeddings/google.js.map +1 -0
  31. package/dist/embeddings/types.d.ts +27 -0
  32. package/dist/embeddings/types.d.ts.map +1 -0
  33. package/dist/embeddings/types.js +2 -0
  34. package/dist/embeddings/types.js.map +1 -0
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +226 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/paths.d.ts +3 -0
  40. package/dist/paths.d.ts.map +1 -0
  41. package/dist/paths.js +10 -0
  42. package/dist/paths.js.map +1 -0
  43. package/dist/server.d.ts +29 -0
  44. package/dist/server.d.ts.map +1 -0
  45. package/dist/server.js +361 -0
  46. package/dist/server.js.map +1 -0
  47. package/dist/tools/confirmation.d.ts +41 -0
  48. package/dist/tools/confirmation.d.ts.map +1 -0
  49. package/dist/tools/confirmation.js +120 -0
  50. package/dist/tools/confirmation.js.map +1 -0
  51. package/dist/tools/constants.d.ts +31 -0
  52. package/dist/tools/constants.d.ts.map +1 -0
  53. package/dist/tools/constants.js +85 -0
  54. package/dist/tools/constants.js.map +1 -0
  55. package/dist/tools/definitions.d.ts +3 -0
  56. package/dist/tools/definitions.d.ts.map +1 -0
  57. package/dist/tools/definitions.js +883 -0
  58. package/dist/tools/definitions.js.map +1 -0
  59. package/dist/tools/handlers/auth.d.ts +3 -0
  60. package/dist/tools/handlers/auth.d.ts.map +1 -0
  61. package/dist/tools/handlers/auth.js +540 -0
  62. package/dist/tools/handlers/auth.js.map +1 -0
  63. package/dist/tools/handlers/component.d.ts +3 -0
  64. package/dist/tools/handlers/component.d.ts.map +1 -0
  65. package/dist/tools/handlers/component.js +145 -0
  66. package/dist/tools/handlers/component.js.map +1 -0
  67. package/dist/tools/handlers/data-sample.d.ts +3 -0
  68. package/dist/tools/handlers/data-sample.d.ts.map +1 -0
  69. package/dist/tools/handlers/data-sample.js +51 -0
  70. package/dist/tools/handlers/data-sample.js.map +1 -0
  71. package/dist/tools/handlers/dynamic.d.ts +3 -0
  72. package/dist/tools/handlers/dynamic.d.ts.map +1 -0
  73. package/dist/tools/handlers/dynamic.js +110 -0
  74. package/dist/tools/handlers/dynamic.js.map +1 -0
  75. package/dist/tools/handlers/flow.d.ts +9 -0
  76. package/dist/tools/handlers/flow.d.ts.map +1 -0
  77. package/dist/tools/handlers/flow.js +899 -0
  78. package/dist/tools/handlers/flow.js.map +1 -0
  79. package/dist/tools/handlers/log.d.ts +3 -0
  80. package/dist/tools/handlers/log.d.ts.map +1 -0
  81. package/dist/tools/handlers/log.js +61 -0
  82. package/dist/tools/handlers/log.js.map +1 -0
  83. package/dist/tools/handlers/organization.d.ts +3 -0
  84. package/dist/tools/handlers/organization.d.ts.map +1 -0
  85. package/dist/tools/handlers/organization.js +39 -0
  86. package/dist/tools/handlers/organization.js.map +1 -0
  87. package/dist/tools/handlers/raw.d.ts +3 -0
  88. package/dist/tools/handlers/raw.d.ts.map +1 -0
  89. package/dist/tools/handlers/raw.js +77 -0
  90. package/dist/tools/handlers/raw.js.map +1 -0
  91. package/dist/tools/handlers/search.d.ts +3 -0
  92. package/dist/tools/handlers/search.d.ts.map +1 -0
  93. package/dist/tools/handlers/search.js +82 -0
  94. package/dist/tools/handlers/search.js.map +1 -0
  95. package/dist/tools/handlers/session.d.ts +3 -0
  96. package/dist/tools/handlers/session.d.ts.map +1 -0
  97. package/dist/tools/handlers/session.js +15 -0
  98. package/dist/tools/handlers/session.js.map +1 -0
  99. package/dist/tools/handlers.d.ts +12 -0
  100. package/dist/tools/handlers.d.ts.map +1 -0
  101. package/dist/tools/handlers.js +197 -0
  102. package/dist/tools/handlers.js.map +1 -0
  103. package/dist/tools/helpers.d.ts +45 -0
  104. package/dist/tools/helpers.d.ts.map +1 -0
  105. package/dist/tools/helpers.js +195 -0
  106. package/dist/tools/helpers.js.map +1 -0
  107. package/dist/tools/types.d.ts +32 -0
  108. package/dist/tools/types.d.ts.map +1 -0
  109. package/dist/tools/types.js +2 -0
  110. package/dist/tools/types.js.map +1 -0
  111. package/dist/tools/validation.d.ts +24 -0
  112. package/dist/tools/validation.d.ts.map +1 -0
  113. package/dist/tools/validation.js +748 -0
  114. package/dist/tools/validation.js.map +1 -0
  115. package/dist/types/index.d.ts +47 -0
  116. package/dist/types/index.d.ts.map +1 -0
  117. package/dist/types/index.js +2 -0
  118. package/dist/types/index.js.map +1 -0
  119. package/dist/vectorstore/factory.d.ts +7 -0
  120. package/dist/vectorstore/factory.d.ts.map +1 -0
  121. package/dist/vectorstore/factory.js +25 -0
  122. package/dist/vectorstore/factory.js.map +1 -0
  123. package/dist/vectorstore/qdrant.d.ts +11 -0
  124. package/dist/vectorstore/qdrant.d.ts.map +1 -0
  125. package/dist/vectorstore/qdrant.js +107 -0
  126. package/dist/vectorstore/qdrant.js.map +1 -0
  127. package/dist/vectorstore/types.d.ts +28 -0
  128. package/dist/vectorstore/types.d.ts.map +1 -0
  129. package/dist/vectorstore/types.js +2 -0
  130. package/dist/vectorstore/types.js.map +1 -0
  131. package/package.json +51 -0
  132. package/resources/docs/component-test-scenarios.md +2468 -0
  133. package/resources/docs/credential-validation-gaps.md +35 -0
  134. package/resources/docs/debug-issues.md +71 -0
  135. package/resources/docs/deployment-and-scaling.md +136 -0
  136. package/resources/docs/flow-building-guide.md +565 -0
  137. package/resources/docs/packaging-plan.md +84 -0
  138. package/resources/docs/standardized-components-guide.md +66 -0
  139. package/resources/docs/system-instructions.md +177 -0
  140. package/resources/docs/unique-components.md +60 -0
  141. package/resources/docs/verified-creds-flow-ids.md +193 -0
  142. package/resources/flow-examples/batch-flow-1-collect.json +51 -0
  143. package/resources/flow-examples/batch-flow-2-process.json +70 -0
  144. package/resources/flow-examples/code-component-transform.json +65 -0
  145. package/resources/flow-examples/code-component-trigger.json +52 -0
  146. package/resources/flow-examples/code-component-with-credentials-trigger.json +53 -0
  147. package/resources/flow-examples/code-component-with-credentials.json +67 -0
  148. package/resources/flow-examples/filter-step.json +63 -0
  149. package/resources/flow-examples/polling-sync-trigger.json +50 -0
  150. package/resources/flow-examples/router-branching.json +94 -0
  151. package/resources/flow-examples/splitter-re-assemble.json +72 -0
  152. package/resources/flow-examples/splitter-split.json +68 -0
  153. package/resources/flow-examples/timer-query-action-sync.json +68 -0
  154. package/resources/flow-examples/webhook-async-sync.json +49 -0
  155. package/resources/flow-examples/webhook-request-reply.json +63 -0
@@ -0,0 +1,899 @@
1
+ import { deserializeJsonApi, deserializeWithMeta } from '../../api/jsonapi.js';
2
+ import { logger } from '../../api/logger.js';
3
+ import { textResult, paginatedResult, formatResult } from '../helpers.js';
4
+ import { validateFlowGraph, checkDiscoveryState, checkFlowLifecycle, checkFlowBuildingGuideRead, healthCheckFlow, formatHealthCheck } from '../validation.js';
5
+ import { PLACEHOLDER_VALUES, PLACEHOLDER_DEFAULT, COMPONENT_SAMPLE_SCHEMAS, PAGE_SIZE } from '../constants.js';
6
+ /**
7
+ * Full domino-effect impact analysis for flow graph changes.
8
+ * Compares existing flow/draft graph with new body and returns a formatted
9
+ * impact report, or null if there are no changes to report.
10
+ */
11
+ export function analyzeFlowImpact(existingFlow, body, newNodes) {
12
+ if (!existingFlow?.graph)
13
+ return null;
14
+ const impactLines = [];
15
+ const prevNodes = existingFlow.graph.nodes || [];
16
+ const prevEdges = existingFlow.graph.edges || [];
17
+ const updatedNodes = newNodes || body?.data?.attributes?.graph?.nodes || [];
18
+ const newEdges = body?.data?.attributes?.graph?.edges || [];
19
+ if (updatedNodes.length === 0 && newEdges.length === 0)
20
+ return null;
21
+ const newNodeIds = new Set(updatedNodes.map((n) => n.id));
22
+ const prevNodeIds = new Set(prevNodes.map((n) => n.id));
23
+ const nodeNameById = new Map(updatedNodes.map((n) => [n.id, n.name || n.id]));
24
+ const prevNodeNameById = new Map(prevNodes.map((n) => [n.id, n.name || n.id]));
25
+ // Extract all $getPassthrough().step_X references from an object
26
+ const extractPassthroughRefs = (obj) => {
27
+ const refs = new Set();
28
+ const str = JSON.stringify(obj || {});
29
+ const re = /\$getPassthrough\(\)\.(\w+)\./g;
30
+ let m;
31
+ while ((m = re.exec(str)) !== null) {
32
+ refs.add(m[1]);
33
+ }
34
+ return refs;
35
+ };
36
+ // Find all steps that reference a given stepId via $getPassthrough()
37
+ const findStepsReferencingId = (stepId) => {
38
+ const result = [];
39
+ for (const edge of newEdges) {
40
+ if (edge.config) {
41
+ const refs = extractPassthroughRefs(edge.config);
42
+ if (refs.has(stepId)) {
43
+ const name = nodeNameById.get(edge.target) || edge.target;
44
+ if (!result.includes(name))
45
+ result.push(name);
46
+ }
47
+ }
48
+ }
49
+ for (const node of updatedNodes) {
50
+ if (node.fields) {
51
+ const refs = extractPassthroughRefs(node.fields);
52
+ if (refs.has(stepId)) {
53
+ const name = nodeNameById.get(node.id) || node.id;
54
+ if (!result.includes(name))
55
+ result.push(name);
56
+ }
57
+ }
58
+ }
59
+ return result;
60
+ };
61
+ // BFS downstream from a step via edges
62
+ const getDownstreamChain = (startId) => {
63
+ const chain = [];
64
+ const visited = new Set();
65
+ const queue = [startId];
66
+ while (queue.length > 0) {
67
+ const current = queue.shift();
68
+ for (const edge of newEdges) {
69
+ if (edge.source === current && !visited.has(edge.target)) {
70
+ visited.add(edge.target);
71
+ chain.push(nodeNameById.get(edge.target) || edge.target);
72
+ queue.push(edge.target);
73
+ }
74
+ }
75
+ }
76
+ return chain;
77
+ };
78
+ // Track all steps whose samples become stale (need refresh via debug_flow_step)
79
+ const staleSteps = new Set();
80
+ // 1. Removed nodes
81
+ for (const prev of prevNodes) {
82
+ if (!newNodeIds.has(prev.id)) {
83
+ const hasSamples = prev.selected_data_samples?.length > 0;
84
+ const referencedBy = findStepsReferencingId(prev.id);
85
+ let line = `REMOVED: "${prev.name || prev.id}"`;
86
+ if (hasSamples)
87
+ line += ` (${prev.selected_data_samples.length} data sample(s) lost)`;
88
+ if (referencedBy.length > 0) {
89
+ line += `\n ⚠ Broken $getPassthrough().${prev.id} references in: ${referencedBy.join(', ')}`;
90
+ referencedBy.forEach(name => {
91
+ const id = updatedNodes.find((n) => (n.name || n.id) === name)?.id;
92
+ if (id)
93
+ staleSteps.add(id);
94
+ });
95
+ }
96
+ impactLines.push(line);
97
+ }
98
+ }
99
+ // 2. New nodes
100
+ for (const node of updatedNodes) {
101
+ if (!prevNodeIds.has(node.id)) {
102
+ const downstream = getDownstreamChain(node.id);
103
+ staleSteps.add(node.id);
104
+ let line = `ADDED: "${node.name || node.id}" — needs data sample via debug_flow_step`;
105
+ if (downstream.length > 0) {
106
+ line += `\n ↳ Downstream steps that depend on this sample: ${downstream.join(' → ')}`;
107
+ downstream.forEach(name => {
108
+ const id = updatedNodes.find((n) => (n.name || n.id) === name)?.id;
109
+ if (id)
110
+ staleSteps.add(id);
111
+ });
112
+ }
113
+ impactLines.push(line);
114
+ }
115
+ }
116
+ // 3. Changed node fields (output schema may change)
117
+ for (const node of updatedNodes) {
118
+ const prev = prevNodes.find((p) => p.id === node.id);
119
+ if (prev && JSON.stringify(prev.fields) !== JSON.stringify(node.fields)) {
120
+ const referencedBy = findStepsReferencingId(node.id);
121
+ const downstream = getDownstreamChain(node.id);
122
+ staleSteps.add(node.id);
123
+ let line = `FIELDS CHANGED: "${node.name || node.id}" — output schema may differ, sample becomes stale`;
124
+ if (referencedBy.length > 0) {
125
+ line += `\n ⚠ Steps reading $getPassthrough().${node.id}: ${referencedBy.join(', ')}`;
126
+ }
127
+ if (downstream.length > 0) {
128
+ line += `\n ↳ Full downstream cascade: ${downstream.join(' → ')}`;
129
+ }
130
+ referencedBy.forEach(name => {
131
+ const id = updatedNodes.find((n) => (n.name || n.id) === name)?.id;
132
+ if (id)
133
+ staleSteps.add(id);
134
+ });
135
+ downstream.forEach(name => {
136
+ const id = updatedNodes.find((n) => (n.name || n.id) === name)?.id;
137
+ if (id)
138
+ staleSteps.add(id);
139
+ });
140
+ impactLines.push(line);
141
+ }
142
+ }
143
+ // 4. Changed/new/removed edges
144
+ const prevEdgeMap = new Map(prevEdges.map((e) => [`${e.source}:${e.target}`, e]));
145
+ const newEdgeKeys = new Set(newEdges.map((e) => `${e.source}:${e.target}`));
146
+ for (const edge of newEdges) {
147
+ const key = `${edge.source}:${edge.target}`;
148
+ const prev = prevEdgeMap.get(key);
149
+ const targetName = nodeNameById.get(edge.target) || edge.target;
150
+ if (!prev) {
151
+ const downstream = getDownstreamChain(edge.target);
152
+ let line = `NEW EDGE: ${nodeNameById.get(edge.source) || edge.source} → ${targetName}`;
153
+ if (edge.config?.condition)
154
+ line += ` [condition: ${edge.config.condition}]`;
155
+ if (downstream.length > 0) {
156
+ line += `\n ↳ Downstream: ${downstream.join(' → ')}`;
157
+ }
158
+ staleSteps.add(edge.target);
159
+ impactLines.push(line);
160
+ }
161
+ else {
162
+ if (edge.config?.mapper && JSON.stringify(prev.config?.mapper) !== JSON.stringify(edge.config.mapper)) {
163
+ const downstream = getDownstreamChain(edge.target);
164
+ staleSteps.add(edge.target);
165
+ let line = `MAPPER CHANGED: → ${targetName} — receives different input data, sample becomes stale`;
166
+ if (downstream.length > 0) {
167
+ line += `\n ↳ Downstream cascade: ${downstream.join(' → ')}`;
168
+ downstream.forEach(name => {
169
+ const id = updatedNodes.find((n) => (n.name || n.id) === name)?.id;
170
+ if (id)
171
+ staleSteps.add(id);
172
+ });
173
+ }
174
+ impactLines.push(line);
175
+ }
176
+ if (JSON.stringify(prev.config?.condition) !== JSON.stringify(edge.config?.condition)) {
177
+ let line = `CONDITION CHANGED: → ${targetName}`;
178
+ line += `\n was: ${prev.config?.condition || '(none)'}`;
179
+ line += `\n now: ${edge.config?.condition || '(none)'}`;
180
+ impactLines.push(line);
181
+ }
182
+ }
183
+ }
184
+ // Removed edges
185
+ for (const prev of prevEdges) {
186
+ const key = `${prev.source}:${prev.target}`;
187
+ if (!newEdgeKeys.has(key)) {
188
+ const sourceName = prevNodeNameById.get(prev.source) || nodeNameById.get(prev.source) || prev.source;
189
+ const targetName = prevNodeNameById.get(prev.target) || nodeNameById.get(prev.target) || prev.target;
190
+ impactLines.push(`REMOVED EDGE: ${sourceName} → ${targetName}`);
191
+ }
192
+ }
193
+ // 5. Samples preserved
194
+ const preservedSteps = updatedNodes.filter((n) => {
195
+ const prev = prevNodes.find((p) => p.id === n.id);
196
+ return prev?.selected_data_samples?.length > 0 && n.selected_data_samples?.length > 0 && !staleSteps.has(n.id);
197
+ });
198
+ if (preservedSteps.length > 0) {
199
+ impactLines.push(`\nUNCHANGED: ${preservedSteps.map((n) => `"${n.name || n.id}"`).join(', ')} — samples retained`);
200
+ }
201
+ // 6. Summary: steps needing sample refresh
202
+ const staleStepNames = [...staleSteps]
203
+ .map(id => nodeNameById.get(id) || id)
204
+ .filter(Boolean);
205
+ if (staleStepNames.length > 0) {
206
+ impactLines.push(`\n⚠ STEPS NEEDING SAMPLE REFRESH (debug_flow_step): ${staleStepNames.join(', ')}`);
207
+ }
208
+ if (impactLines.length === 0)
209
+ return null;
210
+ return `--- Impact Analysis ---\n${impactLines.join('\n')}`;
211
+ }
212
+ /**
213
+ * Infer a plausible placeholder value for a field name based on naming conventions.
214
+ */
215
+ function inferPlaceholderValue(fieldName) {
216
+ const lower = fieldName.toLowerCase();
217
+ for (const entry of PLACEHOLDER_VALUES) {
218
+ if (entry.test(lower)) {
219
+ return typeof entry.value === 'function' ? entry.value() : entry.value;
220
+ }
221
+ }
222
+ return PLACEHOLDER_DEFAULT;
223
+ }
224
+ /**
225
+ * Generate a meaningful sample body for a node by analyzing downstream mapper
226
+ * references and falling back to known component output patterns.
227
+ */
228
+ function generateSampleBody(node, edges, nodesConfig) {
229
+ const nodeId = node.id;
230
+ const command = node.command || '';
231
+ // Check if nodes_config has explicit sample data for this node
232
+ const nodeConfig = nodesConfig[nodeId];
233
+ if (nodeConfig?.sample?.body && Object.keys(nodeConfig.sample.body).length > 0) {
234
+ return nodeConfig.sample.body;
235
+ }
236
+ // Scan all downstream mappers and conditions for references to this node's output.
237
+ // Track both deep field refs ($getPassthrough().nodeId.body.field) and whole body refs
238
+ // ($getPassthrough().nodeId.body). For whole body refs, also track the mapper KEY
239
+ // so we can infer what fields the downstream component expects.
240
+ const referencedFields = new Set();
241
+ const wholeBodyMapperKeys = new Set();
242
+ let hasWholeBodyRef = false;
243
+ for (const edge of edges) {
244
+ const mapper = edge.config?.mapper;
245
+ if (mapper && typeof mapper === 'object' && !Array.isArray(mapper)) {
246
+ for (const [mapperKey, val] of Object.entries(mapper)) {
247
+ if (typeof val !== 'string')
248
+ continue;
249
+ // Match $getPassthrough().nodeId.body.field (deep path)
250
+ const fieldRegex = new RegExp(`\\$getPassthrough\\(\\)\\.${nodeId}\\.body\\.([a-zA-Z_][a-zA-Z0-9_.]*)`, 'g');
251
+ let match;
252
+ while ((match = fieldRegex.exec(val)) !== null) {
253
+ referencedFields.add(match[1].split('.')[0]);
254
+ }
255
+ // Match whole body reference: $getPassthrough().nodeId.body (not followed by .)
256
+ const wholeBodyRegex = new RegExp(`\\$getPassthrough\\(\\)\\.${nodeId}\\.body(?![.a-zA-Z_])`, 'g');
257
+ if (wholeBodyRegex.test(val)) {
258
+ hasWholeBodyRef = true;
259
+ wholeBodyMapperKeys.add(mapperKey);
260
+ }
261
+ }
262
+ }
263
+ else if (typeof mapper === 'string') {
264
+ const fieldRegex = new RegExp(`\\$getPassthrough\\(\\)\\.${nodeId}\\.body\\.([a-zA-Z_][a-zA-Z0-9_.]*)`, 'g');
265
+ let match;
266
+ while ((match = fieldRegex.exec(mapper)) !== null) {
267
+ referencedFields.add(match[1].split('.')[0]);
268
+ }
269
+ const wholeBodyRegex = new RegExp(`\\$getPassthrough\\(\\)\\.${nodeId}\\.body(?![.a-zA-Z_])`, 'g');
270
+ if (wholeBodyRegex.test(mapper)) {
271
+ hasWholeBodyRef = true;
272
+ }
273
+ }
274
+ if (edge.config?.condition && typeof edge.config.condition === 'string') {
275
+ const cond = edge.config.condition;
276
+ const fieldRegex = new RegExp(`\\$getPassthrough\\(\\)\\.${nodeId}\\.body\\.([a-zA-Z_][a-zA-Z0-9_.]*)`, 'g');
277
+ let match;
278
+ while ((match = fieldRegex.exec(cond)) !== null) {
279
+ referencedFields.add(match[1].split('.')[0]);
280
+ }
281
+ }
282
+ }
283
+ // Build sample body from referenced fields
284
+ const body = {};
285
+ for (const field of referencedFields) {
286
+ body[field] = inferPlaceholderValue(field);
287
+ }
288
+ // If downstream references the whole body, use the mapper keys as field names
289
+ // so the UI can render meaningful passthrough pills.
290
+ // E.g. mapper {"idValue": "$getPassthrough().step_1.body"} → sample {idValue: "sample_value"}
291
+ if (hasWholeBodyRef) {
292
+ for (const key of wholeBodyMapperKeys) {
293
+ if (!body[key]) {
294
+ body[key] = inferPlaceholderValue(key);
295
+ }
296
+ }
297
+ // Fallback if no keys were captured
298
+ if (Object.keys(body).length === 0) {
299
+ body.message = 'Sample data';
300
+ body.id = 'sample-id-123';
301
+ }
302
+ }
303
+ // If we found referenced fields, return them
304
+ if (Object.keys(body).length > 0)
305
+ return body;
306
+ // Fall back to known component output patterns
307
+ for (const schema of COMPONENT_SAMPLE_SCHEMAS) {
308
+ if (schema.pattern.test(command)) {
309
+ return schema.sample();
310
+ }
311
+ }
312
+ // Generic fallback
313
+ return { sample: 'auto-generated' };
314
+ }
315
+ const DEBUG_SYNC_TIMEOUT_MS = 30_000;
316
+ /**
317
+ * Execute a single node via /flows/debug-sync and return the output body.
318
+ * Returns null on any failure (timeout, bad creds, component error, etc.).
319
+ */
320
+ async function executeDebugSync(client, workspaceId, node, sampleData) {
321
+ try {
322
+ const nodePayload = {
323
+ id: node.id,
324
+ command: node.command,
325
+ fields: node.fields || {}
326
+ };
327
+ if (node.credentials_id)
328
+ nodePayload.credentials_id = node.credentials_id;
329
+ if (node.secret_id)
330
+ nodePayload.secret_id = node.secret_id;
331
+ const payload = {
332
+ data: {
333
+ type: 'flow',
334
+ attributes: {
335
+ name: `Debug ${node.command}`,
336
+ type: 'debug',
337
+ graph: { nodes: [nodePayload], edges: [] }
338
+ },
339
+ meta: { sampleData },
340
+ relationships: {
341
+ workspace: { data: { id: workspaceId, type: 'workspace' } }
342
+ }
343
+ }
344
+ };
345
+ const response = await client.post('/flows/debug-sync', payload, {
346
+ timeout: DEBUG_SYNC_TIMEOUT_MS,
347
+ __retryCount: 3 // skip retries — debug-sync 500s are component failures, not transient
348
+ });
349
+ const result = response.data?.data?.data || response.data?.data;
350
+ const body = result?.body;
351
+ if (body && typeof body === 'object' && Object.keys(body).length > 0) {
352
+ return body;
353
+ }
354
+ logger.warn(`[executeDebugSync] No usable body for ${node.id}, keys=${JSON.stringify(Object.keys(response.data?.data || {}))}`);
355
+ return null;
356
+ }
357
+ catch (err) {
358
+ logger.warn(`[executeDebugSync] Failed for node ${node.id}: ${err instanceof Error ? err.message : String(err)}`);
359
+ return null;
360
+ }
361
+ }
362
+ /**
363
+ * After creating or updating a flow, automatically create data samples
364
+ * for nodes that don't already have selected_data_samples.
365
+ * Attempts debug-sync retrieval only for new nodes (without existing samples),
366
+ * falls back to synthetic samples on failure.
367
+ */
368
+ async function autoConfirmSamples(flowId, client, discoveryState) {
369
+ try {
370
+ // Get the published flow for workspace info
371
+ const flowRes = await client.get(`/flows/${flowId}`);
372
+ const flow = deserializeJsonApi(flowRes.data);
373
+ const workspaceId = flow?.workspace?.id
374
+ || flow?.workspace?.data?.id;
375
+ if (!workspaceId)
376
+ return;
377
+ // Get or create the draft
378
+ let draft;
379
+ try {
380
+ const draftRes = await client.get(`/flows/${flowId}/draft`);
381
+ draft = deserializeJsonApi(draftRes.data);
382
+ }
383
+ catch {
384
+ // Draft doesn't exist yet — create it from the flow's graph
385
+ const draftAttributes = {
386
+ name: flow.name || flow.attributes?.name || 'Untitled',
387
+ graph: flow.graph
388
+ };
389
+ if (flow.cron || flow.attributes?.cron) {
390
+ draftAttributes.cron = flow.cron || flow.attributes.cron;
391
+ }
392
+ if (flow.type || flow.attributes?.type) {
393
+ draftAttributes.type = flow.type || flow.attributes.type;
394
+ }
395
+ const createDraftRes = await client.put(`/flows/${flowId}/draft`, {
396
+ data: {
397
+ type: 'flow-draft',
398
+ attributes: draftAttributes
399
+ }
400
+ });
401
+ draft = deserializeJsonApi(createDraftRes.data);
402
+ }
403
+ // The draft is always the source of truth for graph structure.
404
+ // autoConfirmSamples only adds missing samples — it never overwrites
405
+ // the draft's nodes, fields, commands, or edges.
406
+ const draftNodes = draft?.graph?.nodes || [];
407
+ if (draftNodes.length === 0)
408
+ return;
409
+ let draftUpdated = false;
410
+ for (const node of draftNodes) {
411
+ // Skip nodes that already have confirmed samples
412
+ if (node.selected_data_samples && node.selected_data_samples.length > 0) {
413
+ continue;
414
+ }
415
+ const componentId = node.component_id;
416
+ if (!componentId)
417
+ continue;
418
+ const commandMatch = node.command?.match(/^[^:]+:([^@]+)@/);
419
+ const method = commandMatch?.[1] || '';
420
+ if (!method)
421
+ continue;
422
+ let versionId = discoveryState.componentVersions.get(componentId);
423
+ if (!versionId) {
424
+ try {
425
+ const compRes = await client.get(`/components/${componentId}`);
426
+ const comp = deserializeJsonApi(compRes.data);
427
+ versionId = comp?.latest_version?.id || comp?.latest_version?.data?.id;
428
+ if (versionId) {
429
+ discoveryState.componentVersions.set(componentId, versionId);
430
+ }
431
+ }
432
+ catch (err) {
433
+ logger.warn(`[autoConfirmSamples] Failed to fetch component ${componentId}: ${err instanceof Error ? err.message : String(err)}`);
434
+ continue;
435
+ }
436
+ }
437
+ if (!versionId)
438
+ continue;
439
+ // Attempt live retrieval via debug-sync — no synthetic fallback
440
+ const sampleBody = await executeDebugSync(client, workspaceId, node, {});
441
+ if (sampleBody) {
442
+ logger.info(`[autoConfirmSamples] Live sample retrieved for node ${node.id}`);
443
+ }
444
+ else {
445
+ logger.warn(`[autoConfirmSamples] No live sample for node ${node.id} — skipping (use debug_flow_step to retrieve manually)`);
446
+ continue;
447
+ }
448
+ try {
449
+ const sampleRes = await client.post('/data-samples', {
450
+ data: {
451
+ type: 'data-sample',
452
+ attributes: {
453
+ result: { body: sampleBody },
454
+ method
455
+ },
456
+ relationships: {
457
+ component: { data: { type: 'component', id: componentId } },
458
+ component_version: { data: { type: 'component-version', id: versionId } },
459
+ workspace: { data: { type: 'workspace', id: workspaceId } }
460
+ }
461
+ }
462
+ });
463
+ const sample = deserializeJsonApi(sampleRes.data);
464
+ if (sample?.id) {
465
+ node.selected_data_samples = [sample.id];
466
+ draftUpdated = true;
467
+ }
468
+ }
469
+ catch (err) {
470
+ logger.warn(`[autoConfirmSamples] Failed to create sample for node ${node.id}: ${err instanceof Error ? err.message : String(err)}`);
471
+ }
472
+ }
473
+ if (draftUpdated) {
474
+ // Always use draft as source of truth — never overwrite draft with published flow data
475
+ const source = draft;
476
+ const updateDraftAttributes = {
477
+ name: source.name || source.attributes?.name || 'Untitled',
478
+ graph: draft.graph
479
+ };
480
+ const cron = source.cron || source.attributes?.cron;
481
+ if (cron) {
482
+ updateDraftAttributes.cron = cron;
483
+ }
484
+ const type = source.type || source.attributes?.type;
485
+ if (type) {
486
+ updateDraftAttributes.type = type;
487
+ }
488
+ if (source.description !== undefined) {
489
+ updateDraftAttributes.description = source.description;
490
+ }
491
+ if (source.nodes_config !== undefined) {
492
+ updateDraftAttributes.nodes_config = source.nodes_config;
493
+ }
494
+ await client.put(`/flows/${flowId}/draft`, {
495
+ data: {
496
+ id: draft.id,
497
+ type: 'flow-draft',
498
+ attributes: updateDraftAttributes
499
+ }
500
+ });
501
+ logger.info(`[autoConfirmSamples] Auto-confirmed synthetic samples for flow ${flowId}`);
502
+ }
503
+ }
504
+ catch (err) {
505
+ // Non-fatal: flow was created/updated successfully, sample confirmation is best-effort
506
+ logger.warn(`[autoConfirmSamples] Failed to auto-confirm samples for flow ${flowId}: ${err instanceof Error ? err.message : String(err)}`);
507
+ }
508
+ }
509
+ export const flowHandlers = {
510
+ create_flow: async (args, { client, discoveryState }) => {
511
+ const { body } = args;
512
+ const guideErrors = checkFlowBuildingGuideRead(discoveryState);
513
+ if (guideErrors.length > 0) {
514
+ throw new Error(`Playbook State Machine Check Failed:\n- ${guideErrors.join('\n- ')}`);
515
+ }
516
+ const { errors: valErrors, warnings: valWarnings } = validateFlowGraph(body);
517
+ if (valErrors.length > 0) {
518
+ throw new Error(`Flow Graph Validation Failed:\n- ${valErrors.join('\n- ')}`);
519
+ }
520
+ const stateErrors = checkDiscoveryState(body, discoveryState);
521
+ if (stateErrors.length > 0) {
522
+ throw new Error(`Playbook State Machine Check Failed:\n- ${stateErrors.join('\n- ')}`);
523
+ }
524
+ // Default cron to every 10 minutes if not specified and flow type is ordinary
525
+ const attrs = body?.data?.attributes;
526
+ if (attrs && !attrs.cron && (!attrs.type || attrs.type === 'ordinary')) {
527
+ attrs.cron = '*/10 * * * *';
528
+ }
529
+ const response = await client.post('/flows', body);
530
+ const data = deserializeJsonApi(response.data);
531
+ if (data?.id) {
532
+ await autoConfirmSamples(data.id, client, discoveryState);
533
+ }
534
+ // Run health check after creation, merging graph validation warnings
535
+ const healthCheck = await healthCheckFlow(data, client);
536
+ healthCheck.warnings.push(...valWarnings);
537
+ const healthSuffix = formatHealthCheck(healthCheck);
538
+ const resultText = formatResult(data) + healthSuffix;
539
+ return { content: [{ type: 'text', text: resultText }] };
540
+ },
541
+ get_flow: async (args, { client, discoveryState }) => {
542
+ const { flowId, include } = args;
543
+ let url = `/flows/${flowId}`;
544
+ if (include !== undefined) {
545
+ url += `?include=${include}`;
546
+ }
547
+ const response = await client.get(url);
548
+ const data = deserializeJsonApi(response.data);
549
+ discoveryState.inspectedFlows.add(flowId);
550
+ // Auto-fetch draft if one exists
551
+ let draft = null;
552
+ if (data?.draft?.data?.id) {
553
+ try {
554
+ const draftRes = await client.get(`/flows/${flowId}/draft`);
555
+ draft = deserializeJsonApi(draftRes.data);
556
+ }
557
+ catch (err) {
558
+ // Draft link exists but fetch failed — skip silently
559
+ }
560
+ }
561
+ return textResult(draft ? { ...data, draft_content: draft } : data);
562
+ },
563
+ get_flow_draft: async (args, { client }) => {
564
+ const { flowId } = args;
565
+ const response = await client.get(`/flows/${flowId}/draft`);
566
+ const data = deserializeJsonApi(response.data);
567
+ return textResult(data);
568
+ },
569
+ list_flows: async (args, { client }) => {
570
+ const { workspaceId, pageSize, pageNumber, search, status, type, componentId, hasDraft, userId, include, sort } = args;
571
+ let url = `/flows?workspace_id=${workspaceId}`;
572
+ const params = [];
573
+ const limit = pageSize !== undefined ? Math.min(pageSize, PAGE_SIZE.FLOWS_MAX) : PAGE_SIZE.FLOWS_DEFAULT;
574
+ params.push(`page[size]=${limit}`);
575
+ if (pageNumber !== undefined)
576
+ params.push(`page[number]=${pageNumber}`);
577
+ if (search !== undefined)
578
+ params.push(`search=${encodeURIComponent(search)}`);
579
+ if (status !== undefined)
580
+ params.push(`filter[status]=${status}`);
581
+ if (type !== undefined)
582
+ params.push(`filter[type]=${type}`);
583
+ if (componentId !== undefined)
584
+ params.push(`filter[component_id]=${componentId}`);
585
+ if (hasDraft !== undefined)
586
+ params.push(`filter[has_draft]=${hasDraft}`);
587
+ if (userId !== undefined)
588
+ params.push(`filter[user]=${userId}`);
589
+ if (include !== undefined)
590
+ params.push(`include=${include}`);
591
+ if (sort !== undefined)
592
+ params.push(`sort=${sort}`);
593
+ if (params.length > 0) {
594
+ url += `&${params.join('&')}`;
595
+ }
596
+ const response = await client.get(url);
597
+ const { data: rawData, meta } = deserializeWithMeta(response.data);
598
+ let flowList = Array.isArray(rawData) ? rawData : (rawData ? [rawData] : []);
599
+ const summary = flowList.map((flow) => ({
600
+ id: flow.id,
601
+ name: flow.name,
602
+ status: flow.status,
603
+ stepCount: flow.graph?.nodes?.length || 0,
604
+ triggerComponent: flow.graph?.nodes?.[0]?.component_id || flow.graph?.nodes?.[0]?.component || 'unknown'
605
+ }));
606
+ return paginatedResult(summary, meta, limit);
607
+ },
608
+ run_flow_now: async (args, { client }) => {
609
+ const { flowId } = args;
610
+ const response = await client.post(`/flows/${flowId}/run-now`);
611
+ const data = deserializeJsonApi(response.data);
612
+ return textResult(data);
613
+ },
614
+ start_flow: async (args, { client }) => {
615
+ const { flowId } = args;
616
+ // Pre-start health check
617
+ const flowRes = await client.get(`/flows/${flowId}`);
618
+ const flow = deserializeJsonApi(flowRes.data);
619
+ const healthCheck = await healthCheckFlow(flow, client);
620
+ if (healthCheck.errors.length > 0) {
621
+ const healthSuffix = formatHealthCheck(healthCheck);
622
+ throw new Error(`Flow cannot be started due to configuration issues:${healthSuffix}`);
623
+ }
624
+ const response = await client.post(`/flows/${flowId}/start`);
625
+ const data = deserializeJsonApi(response.data);
626
+ if (healthCheck.warnings.length > 0) {
627
+ const healthSuffix = formatHealthCheck(healthCheck);
628
+ const resultText = formatResult(data) + healthSuffix;
629
+ return { content: [{ type: 'text', text: resultText }] };
630
+ }
631
+ return textResult(data);
632
+ },
633
+ stop_flow: async (args, { client }) => {
634
+ const { flowId } = args;
635
+ const response = await client.post(`/flows/${flowId}/stop`);
636
+ const data = deserializeJsonApi(response.data);
637
+ return textResult(data);
638
+ },
639
+ update_flow: async (args, { client, discoveryState }) => {
640
+ const { flowId, body } = args;
641
+ const guideErrors = checkFlowBuildingGuideRead(discoveryState);
642
+ if (guideErrors.length > 0) {
643
+ throw new Error(`Playbook State Machine Check Failed:\n- ${guideErrors.join('\n- ')}`);
644
+ }
645
+ let existingFlow = null;
646
+ try {
647
+ const flowRes = await client.get(`/flows/${flowId}`);
648
+ existingFlow = deserializeJsonApi(flowRes.data);
649
+ }
650
+ catch (err) {
651
+ logger.warn(`Failed to fetch existing flow for validation: ${err instanceof Error ? err.message : String(err)}`);
652
+ }
653
+ const lifecycleErrors = checkFlowLifecycle(flowId, discoveryState, existingFlow, 'update_flow');
654
+ if (lifecycleErrors.length > 0) {
655
+ throw new Error(`Flow Lifecycle Check Failed:\n- ${lifecycleErrors.join('\n- ')}`);
656
+ }
657
+ const { errors: valErrors, warnings: valWarnings } = validateFlowGraph(body);
658
+ if (valErrors.length > 0) {
659
+ throw new Error(`Flow Graph Validation Failed:\n- ${valErrors.join('\n- ')}`);
660
+ }
661
+ const stateErrors = checkDiscoveryState(body, discoveryState, existingFlow);
662
+ if (stateErrors.length > 0) {
663
+ throw new Error(`Playbook State Machine Check Failed:\n- ${stateErrors.join('\n- ')}`);
664
+ }
665
+ // Full domino-effect impact analysis before publish
666
+ if (args.confirmed !== true) {
667
+ const impact = analyzeFlowImpact(existingFlow, body);
668
+ if (impact) {
669
+ return {
670
+ isError: true,
671
+ content: [{
672
+ type: 'text',
673
+ text: `⚠️ Confirmation required.\n\nAction: Publish (update) flow ${flowId}.\n\n${impact}\n\nTo proceed, call this tool again with the same parameters and set confirmed: true.`
674
+ }]
675
+ };
676
+ }
677
+ }
678
+ const response = await client.patch(`/flows/${flowId}`, body);
679
+ const data = deserializeJsonApi(response.data);
680
+ await autoConfirmSamples(flowId, client, discoveryState);
681
+ // Run health check after update, merging graph validation warnings
682
+ const healthCheck = await healthCheckFlow(data, client);
683
+ healthCheck.warnings.push(...valWarnings);
684
+ const healthSuffix = formatHealthCheck(healthCheck);
685
+ const resultText = formatResult(data) + healthSuffix;
686
+ return { content: [{ type: 'text', text: resultText }] };
687
+ },
688
+ update_flow_draft: async (args, { client, discoveryState }) => {
689
+ const { flowId, body } = args;
690
+ const guideErrors = checkFlowBuildingGuideRead(discoveryState);
691
+ if (guideErrors.length > 0) {
692
+ throw new Error(`Playbook State Machine Check Failed:\n- ${guideErrors.join('\n- ')}`);
693
+ }
694
+ let existingFlow = null;
695
+ try {
696
+ const draftRes = await client.get(`/flows/${flowId}/draft`);
697
+ existingFlow = deserializeJsonApi(draftRes.data);
698
+ }
699
+ catch (err) {
700
+ try {
701
+ const flowRes = await client.get(`/flows/${flowId}`);
702
+ existingFlow = deserializeJsonApi(flowRes.data);
703
+ }
704
+ catch (e) {
705
+ logger.warn(`Failed to fetch existing flow/draft for validation: ${e instanceof Error ? e.message : String(e)}`);
706
+ }
707
+ }
708
+ const lifecycleErrors = checkFlowLifecycle(flowId, discoveryState, existingFlow, 'update_flow_draft');
709
+ if (lifecycleErrors.length > 0) {
710
+ throw new Error(`Flow Lifecycle Check Failed:\n- ${lifecycleErrors.join('\n- ')}`);
711
+ }
712
+ const { errors: valErrors, warnings: valWarnings } = validateFlowGraph(body);
713
+ if (valErrors.length > 0) {
714
+ throw new Error(`Flow Graph Validation Failed:\n- ${valErrors.join('\n- ')}`);
715
+ }
716
+ const stateErrors = checkDiscoveryState(body, discoveryState, existingFlow);
717
+ if (stateErrors.length > 0) {
718
+ throw new Error(`Playbook State Machine Check Failed:\n- ${stateErrors.join('\n- ')}`);
719
+ }
720
+ // Preserve selected_data_samples from existing draft nodes when not explicitly provided
721
+ const prevNodes = existingFlow?.graph?.nodes || [];
722
+ const updatedNodes = body?.data?.attributes?.graph?.nodes || [];
723
+ for (const node of updatedNodes) {
724
+ if (!node.selected_data_samples) {
725
+ const prev = prevNodes.find((n) => n.id === node.id);
726
+ if (prev?.selected_data_samples?.length > 0) {
727
+ node.selected_data_samples = prev.selected_data_samples;
728
+ }
729
+ }
730
+ }
731
+ // Full domino-effect impact analysis
732
+ if (args.confirmed !== true) {
733
+ const impact = analyzeFlowImpact(existingFlow, body, updatedNodes);
734
+ if (impact) {
735
+ return {
736
+ isError: true,
737
+ content: [{
738
+ type: 'text',
739
+ text: `⚠️ Confirmation required.\n\nAction: Update the draft of flow ${flowId}.\n\n${impact}\n\nTo proceed, call this tool again with the same parameters and set confirmed: true.`
740
+ }]
741
+ };
742
+ }
743
+ }
744
+ const response = await client.put(`/flows/${flowId}/draft`, body);
745
+ const data = deserializeJsonApi(response.data);
746
+ // Auto-create data samples for steps that don't have them.
747
+ // Auto-create data samples for steps that don't have them (draft is always source of truth).
748
+ await autoConfirmSamples(flowId, client, discoveryState);
749
+ // Detect steps with changed configuration or mappings to remind about data sample updates
750
+ const changedSteps = [];
751
+ const existingNodes = existingFlow?.graph?.nodes || [];
752
+ const existingEdges = existingFlow?.graph?.edges || [];
753
+ const newNodes = body?.data?.attributes?.graph?.nodes || [];
754
+ const newEdges = body?.data?.attributes?.graph?.edges || [];
755
+ const existingNodeMap = new Map(existingNodes.map((n) => [n.id, n]));
756
+ const existingEdgeMap = new Map(existingEdges.map((e) => [`${e.source}:${e.target}`, e]));
757
+ for (let i = 0; i < newNodes.length; i++) {
758
+ const node = newNodes[i];
759
+ const prev = existingNodeMap.get(node.id);
760
+ if (!prev || JSON.stringify(prev.fields) !== JSON.stringify(node.fields)) {
761
+ changedSteps.push(node.name || node.id);
762
+ }
763
+ }
764
+ for (let i = 0; i < newEdges.length; i++) {
765
+ const edge = newEdges[i];
766
+ const key = `${edge.source}:${edge.target}`;
767
+ const prev = existingEdgeMap.get(key);
768
+ if (edge.config?.mapper && (!prev?.config?.mapper || JSON.stringify(prev.config.mapper) !== JSON.stringify(edge.config.mapper))) {
769
+ const targetNode = newNodes.find((n) => n.id === edge.target);
770
+ const stepName = targetNode?.name || edge.target;
771
+ if (!changedSteps.includes(stepName)) {
772
+ changedSteps.push(stepName);
773
+ }
774
+ }
775
+ }
776
+ let resultText = formatResult(data);
777
+ if (valWarnings.length > 0) {
778
+ resultText += '\n\n--- Graph Validation Warnings ---\n' + valWarnings.map(w => ` - ${w}`).join('\n');
779
+ }
780
+ if (changedSteps.length > 0) {
781
+ resultText += '\n\n--- Data Sample Reminder ---\n' +
782
+ `The following steps had configuration or mapping changes: ${changedSteps.join(', ')}.\n` +
783
+ 'Ask the user if they want to update/regenerate data samples for these steps (using debug_flow_step or create_data_sample).';
784
+ }
785
+ return { content: [{ type: 'text', text: resultText }] };
786
+ },
787
+ debug_flow_step: async (args, { client, discoveryState }) => {
788
+ const { workspaceId, command, credentialsId, secretId, fields, sampleData, attachments, stepId: customStepId, flowId, componentId } = args;
789
+ const stepId = customStepId || 'step_1';
790
+ const node = {
791
+ id: stepId,
792
+ command,
793
+ error: false
794
+ };
795
+ if (fields && Object.keys(fields).length > 0) {
796
+ node.fields = fields;
797
+ }
798
+ if (credentialsId) {
799
+ node.credentials_id = credentialsId;
800
+ }
801
+ if (secretId) {
802
+ node.secret_id = secretId;
803
+ }
804
+ const inputData = sampleData || {};
805
+ const msgPayload = { body: inputData };
806
+ if (attachments && Object.keys(attachments).length > 0) {
807
+ msgPayload.attachments = attachments;
808
+ }
809
+ const payload = {
810
+ data: {
811
+ type: 'flow',
812
+ attributes: {
813
+ name: `Debug ${command}`,
814
+ type: 'debug',
815
+ graph: {
816
+ nodes: [node],
817
+ edges: []
818
+ },
819
+ variables: {}
820
+ },
821
+ meta: {
822
+ sampleData: msgPayload
823
+ },
824
+ relationships: {
825
+ workspace: {
826
+ data: {
827
+ id: workspaceId,
828
+ type: 'workspace'
829
+ }
830
+ }
831
+ }
832
+ }
833
+ };
834
+ const response = await client.post('/flows/debug-sync', payload, {
835
+ __retryCount: 3 // skip retries — debug-sync 500s are component failures, not transient
836
+ });
837
+ const result = response.data?.data?.data || response.data?.data || response.data;
838
+ const body = result?.body;
839
+ // Save as data sample and update flow draft if flowId and componentId provided
840
+ if (flowId && componentId && body && typeof body === 'object' && Object.keys(body).length > 0) {
841
+ try {
842
+ const commandMatch = command.match(/^[^:]+:([^@]+)@/);
843
+ const method = commandMatch?.[1] || '';
844
+ let versionId = discoveryState.componentVersions.get(componentId);
845
+ if (!versionId) {
846
+ const compRes = await client.get(`/components/${componentId}`);
847
+ const comp = deserializeJsonApi(compRes.data);
848
+ versionId = comp?.latest_version?.id || comp?.latest_version?.data?.id;
849
+ if (versionId)
850
+ discoveryState.componentVersions.set(componentId, versionId);
851
+ }
852
+ if (versionId && method) {
853
+ const sampleRes = await client.post('/data-samples', {
854
+ data: {
855
+ type: 'data-sample',
856
+ attributes: {
857
+ result: { body },
858
+ method
859
+ },
860
+ relationships: {
861
+ component: { data: { type: 'component', id: componentId } },
862
+ component_version: { data: { type: 'component-version', id: versionId } },
863
+ workspace: { data: { type: 'workspace', id: workspaceId } }
864
+ }
865
+ }
866
+ });
867
+ const sample = deserializeJsonApi(sampleRes.data);
868
+ if (sample?.id) {
869
+ // Update draft with the new sample (append to existing)
870
+ const draftRes = await client.get(`/flows/${flowId}/draft`);
871
+ const draft = deserializeJsonApi(draftRes.data);
872
+ const draftNodes = draft?.graph?.nodes || [];
873
+ const targetNode = draftNodes.find((n) => n.id === stepId);
874
+ if (targetNode) {
875
+ targetNode.selected_data_samples = [sample.id];
876
+ const updateAttrs = {
877
+ name: draft.name || 'Untitled',
878
+ graph: draft.graph
879
+ };
880
+ if (draft.cron)
881
+ updateAttrs.cron = draft.cron;
882
+ if (draft.type)
883
+ updateAttrs.type = draft.type;
884
+ await client.put(`/flows/${flowId}/draft`, {
885
+ data: { id: draft.id, type: 'flow-draft', attributes: updateAttrs }
886
+ });
887
+ logger.info(`[debug_flow_step] Saved sample ${sample.id} for node ${stepId} in flow ${flowId}`);
888
+ }
889
+ }
890
+ }
891
+ }
892
+ catch (err) {
893
+ logger.warn(`[debug_flow_step] Failed to save sample for node ${stepId}: ${err instanceof Error ? err.message : String(err)}`);
894
+ }
895
+ }
896
+ return textResult(result);
897
+ }
898
+ };
899
+ //# sourceMappingURL=flow.js.map