@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.
- package/LICENSE +191 -0
- package/README.md +171 -0
- package/dist/api/client.d.ts +12 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +124 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/jsonapi.d.ts +28 -0
- package/dist/api/jsonapi.d.ts.map +1 -0
- package/dist/api/jsonapi.js +45 -0
- package/dist/api/jsonapi.js.map +1 -0
- package/dist/api/logger.d.ts +9 -0
- package/dist/api/logger.d.ts.map +1 -0
- package/dist/api/logger.js +46 -0
- package/dist/api/logger.js.map +1 -0
- package/dist/api/samples.d.ts +21 -0
- package/dist/api/samples.d.ts.map +1 -0
- package/dist/api/samples.js +465 -0
- package/dist/api/samples.js.map +1 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +49 -0
- package/dist/config.js.map +1 -0
- package/dist/embeddings/factory.d.ts +7 -0
- package/dist/embeddings/factory.d.ts.map +1 -0
- package/dist/embeddings/factory.js +25 -0
- package/dist/embeddings/factory.js.map +1 -0
- package/dist/embeddings/google.d.ts +19 -0
- package/dist/embeddings/google.d.ts.map +1 -0
- package/dist/embeddings/google.js +132 -0
- package/dist/embeddings/google.js.map +1 -0
- package/dist/embeddings/types.d.ts +27 -0
- package/dist/embeddings/types.d.ts.map +1 -0
- package/dist/embeddings/types.js +2 -0
- package/dist/embeddings/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +226 -0
- package/dist/index.js.map +1 -0
- package/dist/paths.d.ts +3 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +10 -0
- package/dist/paths.js.map +1 -0
- package/dist/server.d.ts +29 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +361 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/confirmation.d.ts +41 -0
- package/dist/tools/confirmation.d.ts.map +1 -0
- package/dist/tools/confirmation.js +120 -0
- package/dist/tools/confirmation.js.map +1 -0
- package/dist/tools/constants.d.ts +31 -0
- package/dist/tools/constants.d.ts.map +1 -0
- package/dist/tools/constants.js +85 -0
- package/dist/tools/constants.js.map +1 -0
- package/dist/tools/definitions.d.ts +3 -0
- package/dist/tools/definitions.d.ts.map +1 -0
- package/dist/tools/definitions.js +883 -0
- package/dist/tools/definitions.js.map +1 -0
- package/dist/tools/handlers/auth.d.ts +3 -0
- package/dist/tools/handlers/auth.d.ts.map +1 -0
- package/dist/tools/handlers/auth.js +540 -0
- package/dist/tools/handlers/auth.js.map +1 -0
- package/dist/tools/handlers/component.d.ts +3 -0
- package/dist/tools/handlers/component.d.ts.map +1 -0
- package/dist/tools/handlers/component.js +145 -0
- package/dist/tools/handlers/component.js.map +1 -0
- package/dist/tools/handlers/data-sample.d.ts +3 -0
- package/dist/tools/handlers/data-sample.d.ts.map +1 -0
- package/dist/tools/handlers/data-sample.js +51 -0
- package/dist/tools/handlers/data-sample.js.map +1 -0
- package/dist/tools/handlers/dynamic.d.ts +3 -0
- package/dist/tools/handlers/dynamic.d.ts.map +1 -0
- package/dist/tools/handlers/dynamic.js +110 -0
- package/dist/tools/handlers/dynamic.js.map +1 -0
- package/dist/tools/handlers/flow.d.ts +9 -0
- package/dist/tools/handlers/flow.d.ts.map +1 -0
- package/dist/tools/handlers/flow.js +899 -0
- package/dist/tools/handlers/flow.js.map +1 -0
- package/dist/tools/handlers/log.d.ts +3 -0
- package/dist/tools/handlers/log.d.ts.map +1 -0
- package/dist/tools/handlers/log.js +61 -0
- package/dist/tools/handlers/log.js.map +1 -0
- package/dist/tools/handlers/organization.d.ts +3 -0
- package/dist/tools/handlers/organization.d.ts.map +1 -0
- package/dist/tools/handlers/organization.js +39 -0
- package/dist/tools/handlers/organization.js.map +1 -0
- package/dist/tools/handlers/raw.d.ts +3 -0
- package/dist/tools/handlers/raw.d.ts.map +1 -0
- package/dist/tools/handlers/raw.js +77 -0
- package/dist/tools/handlers/raw.js.map +1 -0
- package/dist/tools/handlers/search.d.ts +3 -0
- package/dist/tools/handlers/search.d.ts.map +1 -0
- package/dist/tools/handlers/search.js +82 -0
- package/dist/tools/handlers/search.js.map +1 -0
- package/dist/tools/handlers/session.d.ts +3 -0
- package/dist/tools/handlers/session.d.ts.map +1 -0
- package/dist/tools/handlers/session.js +15 -0
- package/dist/tools/handlers/session.js.map +1 -0
- package/dist/tools/handlers.d.ts +12 -0
- package/dist/tools/handlers.d.ts.map +1 -0
- package/dist/tools/handlers.js +197 -0
- package/dist/tools/handlers.js.map +1 -0
- package/dist/tools/helpers.d.ts +45 -0
- package/dist/tools/helpers.d.ts.map +1 -0
- package/dist/tools/helpers.js +195 -0
- package/dist/tools/helpers.js.map +1 -0
- package/dist/tools/types.d.ts +32 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/validation.d.ts +24 -0
- package/dist/tools/validation.d.ts.map +1 -0
- package/dist/tools/validation.js +748 -0
- package/dist/tools/validation.js.map +1 -0
- package/dist/types/index.d.ts +47 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/vectorstore/factory.d.ts +7 -0
- package/dist/vectorstore/factory.d.ts.map +1 -0
- package/dist/vectorstore/factory.js +25 -0
- package/dist/vectorstore/factory.js.map +1 -0
- package/dist/vectorstore/qdrant.d.ts +11 -0
- package/dist/vectorstore/qdrant.d.ts.map +1 -0
- package/dist/vectorstore/qdrant.js +107 -0
- package/dist/vectorstore/qdrant.js.map +1 -0
- package/dist/vectorstore/types.d.ts +28 -0
- package/dist/vectorstore/types.d.ts.map +1 -0
- package/dist/vectorstore/types.js +2 -0
- package/dist/vectorstore/types.js.map +1 -0
- package/package.json +51 -0
- package/resources/docs/component-test-scenarios.md +2468 -0
- package/resources/docs/credential-validation-gaps.md +35 -0
- package/resources/docs/debug-issues.md +71 -0
- package/resources/docs/deployment-and-scaling.md +136 -0
- package/resources/docs/flow-building-guide.md +565 -0
- package/resources/docs/packaging-plan.md +84 -0
- package/resources/docs/standardized-components-guide.md +66 -0
- package/resources/docs/system-instructions.md +177 -0
- package/resources/docs/unique-components.md +60 -0
- package/resources/docs/verified-creds-flow-ids.md +193 -0
- package/resources/flow-examples/batch-flow-1-collect.json +51 -0
- package/resources/flow-examples/batch-flow-2-process.json +70 -0
- package/resources/flow-examples/code-component-transform.json +65 -0
- package/resources/flow-examples/code-component-trigger.json +52 -0
- package/resources/flow-examples/code-component-with-credentials-trigger.json +53 -0
- package/resources/flow-examples/code-component-with-credentials.json +67 -0
- package/resources/flow-examples/filter-step.json +63 -0
- package/resources/flow-examples/polling-sync-trigger.json +50 -0
- package/resources/flow-examples/router-branching.json +94 -0
- package/resources/flow-examples/splitter-re-assemble.json +72 -0
- package/resources/flow-examples/splitter-split.json +68 -0
- package/resources/flow-examples/timer-query-action-sync.json +68 -0
- package/resources/flow-examples/webhook-async-sync.json +49 -0
- 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
|