@elastic.io/mcp 1.0.1-dev10 → 1.0.1-dev11
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/README.md +37 -19
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +3 -1
- package/dist/api/client.js.map +1 -1
- package/dist/api/verification.d.ts +6 -0
- package/dist/api/verification.d.ts.map +1 -0
- package/dist/api/verification.js +71 -0
- package/dist/api/verification.js.map +1 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +25 -6
- package/dist/setup.js.map +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +34 -2
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/flow-build.d.ts +18 -0
- package/dist/tools/flow-build.d.ts.map +1 -0
- package/dist/tools/flow-build.js +291 -0
- package/dist/tools/flow-build.js.map +1 -0
- package/dist/tools/flow-evidence.d.ts +62 -0
- package/dist/tools/flow-evidence.d.ts.map +1 -0
- package/dist/tools/flow-evidence.js +128 -0
- package/dist/tools/flow-evidence.js.map +1 -0
- package/dist/tools/flow-state-machine.d.ts +68 -0
- package/dist/tools/flow-state-machine.d.ts.map +1 -0
- package/dist/tools/flow-state-machine.js +292 -0
- package/dist/tools/flow-state-machine.js.map +1 -0
- package/dist/tools/handlers/auth.d.ts.map +1 -1
- package/dist/tools/handlers/auth.js +10 -144
- package/dist/tools/handlers/auth.js.map +1 -1
- package/dist/tools/handlers/component.d.ts.map +1 -1
- package/dist/tools/handlers/component.js +26 -6
- package/dist/tools/handlers/component.js.map +1 -1
- package/dist/tools/handlers/data-sample.d.ts.map +1 -1
- package/dist/tools/handlers/data-sample.js +36 -5
- package/dist/tools/handlers/data-sample.js.map +1 -1
- package/dist/tools/handlers/dynamic.d.ts.map +1 -1
- package/dist/tools/handlers/dynamic.js +42 -13
- package/dist/tools/handlers/dynamic.js.map +1 -1
- package/dist/tools/handlers/flow.d.ts.map +1 -1
- package/dist/tools/handlers/flow.js +144 -616
- package/dist/tools/handlers/flow.js.map +1 -1
- package/dist/tools/handlers/log.d.ts.map +1 -1
- package/dist/tools/handlers/log.js.map +1 -1
- package/dist/tools/handlers/raw.d.ts.map +1 -1
- package/dist/tools/handlers/raw.js +3 -0
- package/dist/tools/handlers/raw.js.map +1 -1
- package/dist/tools/handlers/search.d.ts.map +1 -1
- package/dist/tools/handlers/search.js +31 -0
- package/dist/tools/handlers/search.js.map +1 -1
- package/dist/tools/handlers/session.d.ts.map +1 -1
- package/dist/tools/handlers/session.js +7 -1
- package/dist/tools/handlers/session.js.map +1 -1
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +7 -2
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/types.d.ts +6 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/validation.d.ts +16 -1
- package/dist/tools/validation.d.ts.map +1 -1
- package/dist/tools/validation.js +163 -141
- package/dist/tools/validation.js.map +1 -1
- package/package.json +4 -2
- package/resources/docs/flow-creation-stages.md +195 -0
- package/resources/docs/flow-state-machine-audit.md +162 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { inspectFlow, readFlow, saveDraft, publishDraft, validateSample, resampleStaleSteps } from '../flow-build.js';
|
|
2
|
+
import { getFlowBuildStatus, configurationRevision, requireInactive, requireRevision, markRunRequested, schemaErrors } from '../flow-state-machine.js';
|
|
3
|
+
import { fingerprint, operation, schemaFor } from '../flow-evidence.js';
|
|
1
4
|
import { deserializeJsonApi, deserializeWithMeta } from '../../api/jsonapi.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { PLACEHOLDER_VALUES, PLACEHOLDER_DEFAULT, COMPONENT_SAMPLE_SCHEMAS, PAGE_SIZE } from '../constants.js';
|
|
5
|
+
import { textResult, paginatedResult } from '../helpers.js';
|
|
6
|
+
import { validateFlowGraph, checkFlowBuildingGuideRead, healthCheckFlow } from '../validation.js';
|
|
7
|
+
import { PAGE_SIZE } from '../constants.js';
|
|
6
8
|
/**
|
|
7
9
|
* Full domino-effect impact analysis for flow graph changes.
|
|
8
10
|
* Compares existing flow/draft graph with new body and returns a formatted
|
|
@@ -209,362 +211,36 @@ export function analyzeFlowImpact(existingFlow, body, newNodes) {
|
|
|
209
211
|
return null;
|
|
210
212
|
return `--- Impact Analysis ---\n${impactLines.join('\n')}`;
|
|
211
213
|
}
|
|
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
214
|
export const flowHandlers = {
|
|
510
215
|
create_flow: async (args, { client, discoveryState }) => {
|
|
511
216
|
const { body } = args;
|
|
512
217
|
const guideErrors = checkFlowBuildingGuideRead(discoveryState);
|
|
513
|
-
if (guideErrors.length
|
|
514
|
-
throw new Error(
|
|
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
|
|
218
|
+
if (guideErrors.length)
|
|
219
|
+
throw new Error(guideErrors.join('\n'));
|
|
525
220
|
const attrs = body?.data?.attributes;
|
|
526
|
-
if (attrs
|
|
527
|
-
|
|
221
|
+
if (!attrs || 'graph' in attrs || 'nodes' in attrs || 'edges' in attrs) {
|
|
222
|
+
throw new Error('create_flow creates an empty flow. Supply minimal attributes without graph; build the draft with update_flow_draft.');
|
|
528
223
|
}
|
|
224
|
+
if (!attrs.cron && (!attrs.type || attrs.type === 'ordinary'))
|
|
225
|
+
attrs.cron = '*/10 * * * *';
|
|
529
226
|
const response = await client.post('/flows', body);
|
|
530
227
|
const data = deserializeJsonApi(response.data);
|
|
531
|
-
if (data?.id)
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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 }] };
|
|
228
|
+
if (!data?.id)
|
|
229
|
+
throw new Error('Create returned no flow ID. Inspect existing flows before retrying to avoid a duplicate.');
|
|
230
|
+
const current = await inspectFlow(client, discoveryState, data.id);
|
|
231
|
+
return textResult({ ...current.flow, build: await getFlowBuildStatus(discoveryState, data.id) });
|
|
540
232
|
},
|
|
541
233
|
get_flow: async (args, { client, discoveryState }) => {
|
|
542
|
-
const
|
|
543
|
-
|
|
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);
|
|
234
|
+
const current = await inspectFlow(client, discoveryState, args.flowId);
|
|
235
|
+
return textResult({ ...current.flow, draft_content: current.draft, build: await getFlowBuildStatus(discoveryState, args.flowId) });
|
|
562
236
|
},
|
|
563
|
-
get_flow_draft: async (args, { client }) => {
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
237
|
+
get_flow_draft: async (args, { client, discoveryState }) => {
|
|
238
|
+
const current = await inspectFlow(client, discoveryState, args.flowId);
|
|
239
|
+
return textResult({ draft: current.draft, build: await getFlowBuildStatus(discoveryState, args.flowId) });
|
|
240
|
+
},
|
|
241
|
+
get_flow_build_status: async (args, { client, discoveryState }) => {
|
|
242
|
+
await inspectFlow(client, discoveryState, args.flowId);
|
|
243
|
+
return textResult(await getFlowBuildStatus(discoveryState, args.flowId));
|
|
568
244
|
},
|
|
569
245
|
list_flows: async (args, { client }) => {
|
|
570
246
|
const { workspaceId, pageSize, pageNumber, search, status, type, componentId, hasDraft, userId, include, sort } = args;
|
|
@@ -605,295 +281,147 @@ export const flowHandlers = {
|
|
|
605
281
|
}));
|
|
606
282
|
return paginatedResult(summary, meta, limit);
|
|
607
283
|
},
|
|
608
|
-
run_flow_now: async (args, { client }) => {
|
|
609
|
-
const
|
|
610
|
-
|
|
284
|
+
run_flow_now: async (args, { client, discoveryState }) => {
|
|
285
|
+
const current = await inspectFlow(client, discoveryState, args.flowId);
|
|
286
|
+
if (current.runtime !== 'ACTIVE')
|
|
287
|
+
throw new Error('Flow must be ACTIVE before run_flow_now.');
|
|
288
|
+
const response = await client.post(`/flows/${args.flowId}/run-now`);
|
|
611
289
|
const data = deserializeJsonApi(response.data);
|
|
612
|
-
|
|
290
|
+
markRunRequested(discoveryState, args.flowId, data?.execution_id || data?.execution?.id);
|
|
291
|
+
return textResult({ result: data, verification: 'PENDING', message: 'Execution requested. Successful completion has not been verified.' });
|
|
613
292
|
},
|
|
614
|
-
start_flow: async (args, { client }) => {
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
293
|
+
start_flow: async (args, { client, discoveryState }) => {
|
|
294
|
+
const current = await inspectFlow(client, discoveryState, args.flowId);
|
|
295
|
+
if (current.runtime === 'ACTIVE')
|
|
296
|
+
return textResult({ ...current.flow, alreadyActive: true });
|
|
297
|
+
requireInactive(current);
|
|
298
|
+
if (current.draft) {
|
|
299
|
+
const draftGraph = configurationRevision({ graph: current.draft.graph }, false);
|
|
300
|
+
const flowGraph = configurationRevision({ graph: current.flow.graph }, false);
|
|
301
|
+
if (draftGraph !== flowGraph) {
|
|
302
|
+
throw new Error('Unpublished draft changes exist. Publish the validated draft first.');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const structural = validateFlowGraph({ graph: current.flow.graph });
|
|
306
|
+
const health = await healthCheckFlow(current.flow, client, discoveryState);
|
|
307
|
+
const errors = [...structural.errors, ...health.errors];
|
|
308
|
+
if (errors.length)
|
|
309
|
+
throw new Error(`Flow cannot be started:\n- ${errors.join('\n- ')}`);
|
|
310
|
+
const latest = await readFlow(client, args.flowId);
|
|
311
|
+
requireRevision(latest, current.revision);
|
|
312
|
+
requireInactive(latest);
|
|
313
|
+
await client.post(`/flows/${args.flowId}/start`);
|
|
314
|
+
const started = await inspectFlow(client, discoveryState, args.flowId);
|
|
315
|
+
return textResult({ ...started.flow, operation: 'start_requested', runtime: started.runtime });
|
|
632
316
|
},
|
|
633
|
-
stop_flow: async (args, { client }) => {
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
317
|
+
stop_flow: async (args, { client, discoveryState }) => {
|
|
318
|
+
const current = await inspectFlow(client, discoveryState, args.flowId);
|
|
319
|
+
if (current.runtime === 'INACTIVE')
|
|
320
|
+
return textResult({ ...current.flow, alreadyInactive: true });
|
|
321
|
+
if (current.runtime === 'UNKNOWN')
|
|
322
|
+
throw new Error('Cannot stop flow with unknown runtime status. Inspect again.');
|
|
323
|
+
if (current.runtime !== 'STOPPING')
|
|
324
|
+
await client.post(`/flows/${args.flowId}/stop`);
|
|
325
|
+
const stopped = await inspectFlow(client, discoveryState, args.flowId);
|
|
326
|
+
return textResult({ ...stopped.flow, operation: 'stop_requested', runtime: stopped.runtime });
|
|
638
327
|
},
|
|
639
328
|
update_flow: async (args, { client, discoveryState }) => {
|
|
640
|
-
const { flowId, body } = args;
|
|
641
329
|
const guideErrors = checkFlowBuildingGuideRead(discoveryState);
|
|
642
|
-
if (guideErrors.length
|
|
643
|
-
throw new Error(
|
|
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
|
|
330
|
+
if (guideErrors.length)
|
|
331
|
+
throw new Error(guideErrors.join('\n'));
|
|
666
332
|
if (args.confirmed !== true) {
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
}
|
|
333
|
+
const current = await inspectFlow(client, discoveryState, args.flowId);
|
|
334
|
+
requireRevision(current, args.expectedRevision);
|
|
335
|
+
return { ...textResult({ preview: analyzeFlowImpact(current.flow, args.body), revision: current.revision,
|
|
336
|
+
message: 'Publish the saved draft with confirmed: true and this expectedRevision.' }), isError: true };
|
|
677
337
|
}
|
|
678
|
-
|
|
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 }] };
|
|
338
|
+
return textResult(await publishDraft(client, discoveryState, args));
|
|
687
339
|
},
|
|
688
340
|
update_flow_draft: async (args, { client, discoveryState }) => {
|
|
689
|
-
const { flowId, body } = args;
|
|
690
341
|
const guideErrors = checkFlowBuildingGuideRead(discoveryState);
|
|
691
|
-
if (guideErrors.length
|
|
692
|
-
throw new Error(
|
|
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
|
|
342
|
+
if (guideErrors.length)
|
|
343
|
+
throw new Error(guideErrors.join('\n'));
|
|
732
344
|
if (args.confirmed !== true) {
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
isError: true
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
345
|
+
const current = await inspectFlow(client, discoveryState, args.flowId);
|
|
346
|
+
requireRevision(current, args.expectedRevision);
|
|
347
|
+
return { ...textResult({ preview: analyzeFlowImpact(current.draft || current.flow, args.body), revision: current.revision,
|
|
348
|
+
message: 'Save the draft with confirmed: true and this expectedRevision.' }), isError: true };
|
|
349
|
+
}
|
|
350
|
+
const saved = await saveDraft(client, discoveryState, args);
|
|
351
|
+
// Impact analysis: compare what was submitted against what existed before.
|
|
352
|
+
// saveDraft already reconciled the state, so we can compute impact from the
|
|
353
|
+
// submitted body alone (analyzeFlowImpact only needs the existing graph + body).
|
|
354
|
+
const result = { ...saved };
|
|
355
|
+
// Auto-resample steps whose samples were invalidated by the draft change.
|
|
356
|
+
// Steps with precondition blockers (credentials, metadata) are skipped.
|
|
357
|
+
const resample = await resampleStaleSteps(client, discoveryState, args.flowId);
|
|
358
|
+
if (resample.resampled.length || resample.failed.length) {
|
|
359
|
+
result.resample = resample;
|
|
360
|
+
result.build = await getFlowBuildStatus(discoveryState, args.flowId);
|
|
743
361
|
}
|
|
744
|
-
|
|
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 }] };
|
|
362
|
+
return textResult(result);
|
|
786
363
|
},
|
|
787
364
|
debug_flow_step: async (args, { client, discoveryState }) => {
|
|
788
|
-
const { workspaceId, command, credentialsId, secretId, fields, sampleData, attachments, stepId
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
365
|
+
const { workspaceId, command, credentialsId, secretId, fields = {}, sampleData, attachments, stepId = 'step_1', flowId, componentId } = args;
|
|
366
|
+
let current;
|
|
367
|
+
if (flowId) {
|
|
368
|
+
current = await inspectFlow(client, discoveryState, flowId);
|
|
369
|
+
requireRevision(current, args.expectedRevision);
|
|
370
|
+
requireInactive(current);
|
|
371
|
+
const node = current.draft?.graph?.nodes?.find((n) => n.id === stepId);
|
|
372
|
+
if (!node || !componentId)
|
|
373
|
+
throw new Error('To attach a debug sample, select an existing draft step and its componentId.');
|
|
374
|
+
const expected = { command: node.command, component_id: node.component_id, fields: node.fields || {},
|
|
375
|
+
credentials_id: node.credentials_id, secret_id: node.secret_id };
|
|
376
|
+
const supplied = { command, component_id: componentId, fields, credentials_id: credentialsId, secret_id: secretId };
|
|
377
|
+
if (current.workspaceId !== workspaceId || fingerprint(expected) !== fingerprint(supplied)) {
|
|
378
|
+
throw new Error('Debug configuration does not match the current draft step.');
|
|
379
|
+
}
|
|
380
|
+
if (current.draft.graph.edges.some((e) => e.target === stepId) && sampleData === undefined) {
|
|
381
|
+
throw new Error('Provide sampleData from the upstream mapper when debugging an action.');
|
|
382
|
+
}
|
|
383
|
+
const inputErrors = schemaErrors(schemaFor(discoveryState, workspaceId, node)?.in, sampleData || {});
|
|
384
|
+
if (inputErrors.length)
|
|
385
|
+
throw new Error(`Invalid debug input: ${inputErrors.join('; ')}`);
|
|
386
|
+
}
|
|
387
|
+
const node = { id: stepId, command, fields, error: false };
|
|
388
|
+
if (credentialsId)
|
|
799
389
|
node.credentials_id = credentialsId;
|
|
800
|
-
|
|
801
|
-
if (secretId) {
|
|
390
|
+
if (secretId)
|
|
802
391
|
node.secret_id = secretId;
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
-
});
|
|
392
|
+
const response = await client.post('/flows/debug-sync', { data: {
|
|
393
|
+
type: 'flow', attributes: { name: `Debug ${command}`, type: 'debug', graph: { nodes: [node], edges: [] }, variables: {} },
|
|
394
|
+
meta: { sampleData: { body: sampleData || {}, ...(attachments ? { attachments } : {}) } },
|
|
395
|
+
relationships: { workspace: { data: { id: workspaceId, type: 'workspace' } } },
|
|
396
|
+
} });
|
|
837
397
|
const result = response.data?.data?.data || response.data?.data || response.data;
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
if (
|
|
398
|
+
if (result?.error)
|
|
399
|
+
return { ...textResult(result), isError: true };
|
|
400
|
+
if (current && result?.body !== undefined) {
|
|
401
|
+
const op = operation({ command, component_id: componentId });
|
|
402
|
+
const sampleResponse = await client.post('/data-samples', { data: {
|
|
403
|
+
type: 'data-sample', attributes: { result: { body: result.body }, method: op.method },
|
|
404
|
+
relationships: { component: { data: { type: 'component', id: componentId } },
|
|
405
|
+
component_version: { data: { type: 'component-version', id: op.version } },
|
|
406
|
+
workspace: { data: { type: 'workspace', id: workspaceId } } },
|
|
407
|
+
} });
|
|
408
|
+
const sample = deserializeJsonApi(sampleResponse.data);
|
|
409
|
+
if (!sample?.id)
|
|
410
|
+
throw new Error('Debug succeeded, but creating its sample returned no ID.');
|
|
411
|
+
const graph = structuredClone(current.draft.graph);
|
|
412
|
+
graph.nodes.find((n) => n.id === stepId).selected_data_samples = [sample.id];
|
|
841
413
|
try {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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
|
-
}
|
|
414
|
+
await saveDraft(client, discoveryState, { flowId, expectedRevision: current.revision,
|
|
415
|
+
body: { data: { type: 'flow-draft', attributes: { graph } } } });
|
|
416
|
+
const saved = await inspectFlow(client, discoveryState, flowId);
|
|
417
|
+
validateSample(discoveryState, flowId, saved, stepId, sample);
|
|
418
|
+
return textResult({ result, sampleId: sample.id, build: await getFlowBuildStatus(discoveryState, flowId) });
|
|
891
419
|
}
|
|
892
|
-
catch (
|
|
893
|
-
|
|
420
|
+
catch (error) {
|
|
421
|
+
throw new Error(`Debug sample ${sample.id} was created, but attachment/validation failed: ${error.message}. Inspect before retrying the debug action.`);
|
|
894
422
|
}
|
|
895
423
|
}
|
|
896
|
-
return textResult(result);
|
|
424
|
+
return textResult({ result, sampleSaved: false });
|
|
897
425
|
}
|
|
898
426
|
};
|
|
899
427
|
//# sourceMappingURL=flow.js.map
|