flowmind 1.5.1 → 1.5.3

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.
@@ -3,6 +3,10 @@
3
3
  * Route deployment and workflow requests to the configured workflow MCP adapter.
4
4
  */
5
5
 
6
+ const fs = require('fs');
7
+ const os = require('os');
8
+ const path = require('path');
9
+
6
10
  const BUILTIN_WORKFLOWS = {
7
11
  'dev-workflow': {
8
12
  name: 'Development Workflow',
@@ -39,6 +43,21 @@ const TOOL_NAMES = {
39
43
  listPipelineRuns: 'flowListPipelineRuns'
40
44
  };
41
45
 
46
+ const LOCAL_PIPELINE_MAP_CANDIDATES = [
47
+ process.env.FLOWMIND_AUTO_FLOW_MAP,
48
+ path.join(process.env.FLOWMIND_HOME || process.env.HOME || process.env.USERPROFILE || os.homedir(), '.flowmind', 'source', 'auto-flow-pipeline-map.json'),
49
+ path.join(process.cwd(), 'source', 'auto-flow-pipeline-map.json'),
50
+ path.join(__dirname, '..', '..', '..', 'source', 'auto-flow-pipeline-map.json')
51
+ ].filter(Boolean);
52
+
53
+ const ENV_PREFIX_TO_ENV = {
54
+ test: 'test',
55
+ dev: 'test',
56
+ uat: 'uat',
57
+ gray: 'gray',
58
+ prod: 'prod'
59
+ };
60
+
42
61
  module.exports = {
43
62
  canHandle(input) {
44
63
  if (!input) return false;
@@ -77,27 +96,39 @@ module.exports = {
77
96
  }
78
97
 
79
98
  if (params.action === 'status') {
80
- return executeStatus(workflow, params, input);
99
+ await executeStatus(workflow, params, input);
100
+ return {
101
+ type: 'result',
102
+ skill: 'auto-flow',
103
+ message: 'Workflow status: query completed',
104
+ data: {
105
+ action: 'status',
106
+ provider: workflow.provider,
107
+ binding: workflow.binding,
108
+ status: 'completed',
109
+ mcpServer: getWorkflowMcpServer(workflow)
110
+ },
111
+ input,
112
+ timestamp: new Date().toISOString()
113
+ };
81
114
  }
82
115
 
83
116
  if (params.action === 'deploy' || params.action === 'run') {
84
- return executeDeploy(workflow, params, input);
117
+ return executeDeploy(workflow, params, input, context);
85
118
  }
86
119
 
87
120
  if (params.action === 'list' || params.serviceNames.length > 0) {
88
- const execution = await workflow.client.listPipelines(buildListPipelineParams(params));
121
+ await workflow.client.listPipelines(buildListPipelineParams(params));
89
122
  return {
90
123
  type: 'result',
91
124
  skill: 'auto-flow',
92
- message: params.serviceNames.length > 0
93
- ? `Resolved workflow query for: ${params.serviceNames.join(', ')}`
94
- : 'Listing available deployment pipelines',
125
+ message: 'Workflow status: pipeline lookup completed',
95
126
  data: {
96
127
  action: 'list',
97
128
  provider: workflow.provider,
98
129
  binding: workflow.binding,
99
- filters: buildListPipelineParams(params),
100
- execution
130
+ status: 'completed',
131
+ mcpServer: getWorkflowMcpServer(workflow)
101
132
  },
102
133
  input,
103
134
  timestamp: new Date().toISOString()
@@ -147,7 +178,8 @@ function createWorkflowClient(context) {
147
178
  return {
148
179
  client: adapter,
149
180
  provider: adapter.providerName,
150
- binding: context.resourceBinding?.componentType === 'workflow' ? context.resourceBinding : null
181
+ binding: context.resourceBinding?.componentType === 'workflow' ? context.resourceBinding : null,
182
+ transportBacked: true
151
183
  };
152
184
  }
153
185
 
@@ -156,7 +188,7 @@ function createWorkflowClient(context) {
156
188
  : null;
157
189
 
158
190
  if (!binding?.mcpServer) {
159
- return { client: null, provider: null, binding: null };
191
+ return { client: null, provider: null, binding: null, transportBacked: false };
160
192
  }
161
193
 
162
194
  return {
@@ -165,6 +197,15 @@ function createWorkflowClient(context) {
165
197
  async listPipelines(params) {
166
198
  return { mcpServer: binding.mcpServer, tool: TOOL_NAMES.listPipelines, params };
167
199
  },
200
+ async getCurrentIterate() {
201
+ return { mcpServer: binding.mcpServer, tool: 'getCurrentIterate', params: {} };
202
+ },
203
+ async listDeployChecklists(params) {
204
+ return { mcpServer: binding.mcpServer, tool: 'listDeployChecklists', params };
205
+ },
206
+ async orderList(params) {
207
+ return { mcpServer: binding.mcpServer, tool: 'orderList', params };
208
+ },
168
209
  async startPipelineRun(pipelineId) {
169
210
  return { mcpServer: binding.mcpServer, tool: TOOL_NAMES.startPipelineRun, params: { pipelineId } };
170
211
  },
@@ -179,7 +220,8 @@ function createWorkflowClient(context) {
179
220
  }
180
221
  },
181
222
  provider: binding.provider || 'workflow-binding',
182
- binding
223
+ binding,
224
+ transportBacked: false
183
225
  };
184
226
  }
185
227
 
@@ -204,7 +246,7 @@ function buildNoAdapterResult(input, params) {
204
246
  skill: 'auto-flow',
205
247
  message: 'Workflow service not configured. Connect friday-auto-flow first.',
206
248
  data: {
207
- params,
249
+ status: 'not_configured',
208
250
  hint: 'Run `flowmind resource` to review current bindings, then save one like: `flowmind "绑定发布业务 mcp=friday-auto-flow token=xxx env=uat"`'
209
251
  },
210
252
  input,
@@ -212,20 +254,53 @@ function buildNoAdapterResult(input, params) {
212
254
  };
213
255
  }
214
256
 
215
- async function executeDeploy(workflow, params, input) {
257
+ async function executeDeploy(workflow, params, input, context) {
216
258
  if (params.pipelineId) {
217
259
  const execution = await workflow.client.startPipelineRun(params.pipelineId);
218
260
  return {
219
261
  type: 'result',
220
262
  skill: 'auto-flow',
221
- message: `Starting pipeline ${params.pipelineId}`,
263
+ message: 'Workflow status: deployment submitted',
264
+ data: {
265
+ action: 'deploy',
266
+ provider: workflow.provider,
267
+ binding: workflow.binding,
268
+ status: 'submitted',
269
+ mcpServer: getWorkflowMcpServer(workflow),
270
+ execution,
271
+ resolution: {
272
+ source: 'direct-input',
273
+ pipelineId: params.pipelineId
274
+ }
275
+ },
276
+ input,
277
+ timestamp: new Date().toISOString()
278
+ };
279
+ }
280
+
281
+ const resolvedTargets = await resolvePipelineTargets(workflow, params, context);
282
+ if (resolvedTargets.pipelineIds.length > 0) {
283
+ const executions = [];
284
+ for (const pipelineId of resolvedTargets.pipelineIds) {
285
+ executions.push(await workflow.client.startPipelineRun(pipelineId));
286
+ }
287
+
288
+ return {
289
+ type: 'result',
290
+ skill: 'auto-flow',
291
+ message: 'Workflow status: deployment submitted',
222
292
  data: {
223
293
  action: 'deploy',
224
- pipelineId: params.pipelineId,
225
- environment: params.environment,
226
294
  provider: workflow.provider,
227
295
  binding: workflow.binding,
228
- execution
296
+ status: 'submitted',
297
+ mcpServer: getWorkflowMcpServer(workflow),
298
+ execution: executions.length === 1 ? executions[0] : executions,
299
+ resolution: {
300
+ source: resolvedTargets.source,
301
+ pipelineIds: resolvedTargets.pipelineIds,
302
+ services: resolvedTargets.resolutions
303
+ }
229
304
  },
230
305
  input,
231
306
  timestamp: new Date().toISOString()
@@ -233,61 +308,52 @@ async function executeDeploy(workflow, params, input) {
233
308
  }
234
309
 
235
310
  const listParams = buildListPipelineParams(params);
236
- const resolveExecution = await workflow.client.listPipelines(listParams);
311
+ const lookup = await workflow.client.listPipelines(listParams);
237
312
  const batchParams = buildBatchRunParams(params);
238
313
  const execution = await workflow.client.startBatchPipelineRun(batchParams);
239
314
 
240
- return {
241
- type: 'result',
242
- skill: 'auto-flow',
243
- message: params.serviceNames.length > 0
244
- ? `Prepared deployment for: ${params.serviceNames.join(', ')}`
245
- : `Prepared workflow deployment${params.workflow ? `: ${params.workflow}` : ''}`,
246
- data: {
247
- action: 'deploy',
248
- services: params.serviceNames,
249
- workflow: params.workflow,
250
- environment: params.environment,
251
- provider: workflow.provider,
252
- binding: workflow.binding,
253
- resolution: {
254
- filters: listParams,
255
- execution: resolveExecution
315
+ return {
316
+ type: 'result',
317
+ skill: 'auto-flow',
318
+ message: 'Workflow status: deployment submitted',
319
+ data: {
320
+ action: 'deploy',
321
+ provider: workflow.provider,
322
+ binding: workflow.binding,
323
+ status: 'submitted',
324
+ mcpServer: getWorkflowMcpServer(workflow),
325
+ execution,
326
+ resolution: {
327
+ source: 'batch-fallback',
328
+ lookup
329
+ }
256
330
  },
257
- execution
258
- },
259
- input,
260
- timestamp: new Date().toISOString()
261
- };
262
- }
331
+ input,
332
+ timestamp: new Date().toISOString()
333
+ };
334
+ }
263
335
 
264
336
  async function executeStatus(workflow, params, input) {
265
- let execution;
266
337
  if (params.pipelineId && params.runId) {
267
- execution = await workflow.client.getPipelineRun(params.pipelineId, params.runId);
338
+ await workflow.client.getPipelineRun(params.pipelineId, params.runId);
268
339
  } else if (params.pipelineId) {
269
- execution = await workflow.client.listPipelineRuns(params.pipelineId, {
340
+ await workflow.client.listPipelineRuns(params.pipelineId, {
270
341
  env: params.environment
271
342
  });
272
343
  } else {
273
- execution = await workflow.client.listPipelines(buildListPipelineParams(params));
344
+ await workflow.client.listPipelines(buildListPipelineParams(params));
274
345
  }
275
346
 
276
347
  return {
277
348
  type: 'result',
278
349
  skill: 'auto-flow',
279
- message: params.runId
280
- ? `Querying run status for ${params.runId}`
281
- : 'Querying pipeline status',
350
+ message: 'Workflow status: query completed',
282
351
  data: {
283
352
  action: 'status',
284
- services: params.serviceNames,
285
- pipelineId: params.pipelineId,
286
- runId: params.runId,
287
- environment: params.environment,
288
353
  provider: workflow.provider,
289
354
  binding: workflow.binding,
290
- execution
355
+ status: 'completed',
356
+ mcpServer: workflow.binding?.mcpServer || workflow.provider
291
357
  },
292
358
  input,
293
359
  timestamp: new Date().toISOString()
@@ -337,6 +403,7 @@ function parseFlowParams(input) {
337
403
  }
338
404
 
339
405
  params.serviceNames = extractServiceNames(input);
406
+ applyPrefixedServiceNormalization(params);
340
407
 
341
408
  if (!params.workflow && /deploy-workflow|dev-workflow/i.test(input)) {
342
409
  params.workflow = input.match(/deploy-workflow|dev-workflow/i)[0];
@@ -394,6 +461,415 @@ function normalizeEnvironment(value) {
394
461
  return normalized;
395
462
  }
396
463
 
464
+ function applyPrefixedServiceNormalization(params) {
465
+ const normalizedServices = [];
466
+
467
+ for (const originalName of params.serviceNames) {
468
+ const prefixed = splitPrefixedServiceName(originalName);
469
+ if (prefixed) {
470
+ if (!params.environment) {
471
+ params.environment = prefixed.environment;
472
+ }
473
+ normalizedServices.push(prefixed.serviceName);
474
+ continue;
475
+ }
476
+ normalizedServices.push(originalName);
477
+ }
478
+
479
+ params.serviceNames = [...new Set(normalizedServices)];
480
+ }
481
+
482
+ function splitPrefixedServiceName(serviceName) {
483
+ const match = String(serviceName || '').match(/^(test|dev|uat|gray|prod)-(.+)$/i);
484
+ if (!match || !match[2] || !match[2].includes('-')) {
485
+ return null;
486
+ }
487
+
488
+ return {
489
+ environment: ENV_PREFIX_TO_ENV[match[1].toLowerCase()] || normalizeEnvironment(match[1]),
490
+ serviceName: match[2]
491
+ };
492
+ }
493
+
494
+ async function resolvePipelineTargets(workflow, params, context) {
495
+ if (params.serviceNames.length === 0) {
496
+ return { source: null, resolutions: [], pipelineIds: [] };
497
+ }
498
+
499
+ const resolutions = [];
500
+ let source = null;
501
+
502
+ for (const serviceName of params.serviceNames) {
503
+ const resolution = await resolvePipelineForService(workflow, {
504
+ serviceName,
505
+ environment: params.environment,
506
+ context
507
+ });
508
+
509
+ if (!resolution?.pipelineId) {
510
+ return { source: null, resolutions: [], pipelineIds: [] };
511
+ }
512
+
513
+ source = source || resolution.source;
514
+ resolutions.push(resolution);
515
+ }
516
+
517
+ return {
518
+ source,
519
+ resolutions,
520
+ pipelineIds: [...new Set(resolutions.map((item) => String(item.pipelineId)))]
521
+ };
522
+ }
523
+
524
+ async function resolvePipelineForService(workflow, { serviceName, environment, context }) {
525
+ const localResolution = resolveLocalPipelineForService(serviceName, environment, context);
526
+ if (localResolution) {
527
+ return localResolution;
528
+ }
529
+
530
+ if (!workflow.transportBacked || typeof workflow.client?.getCurrentIterate !== 'function') {
531
+ return null;
532
+ }
533
+
534
+ return resolveRemotePipelineForService(workflow.client, serviceName, environment);
535
+ }
536
+
537
+ function resolveLocalPipelineForService(serviceName, environment, context) {
538
+ const pipelineMap = readLocalPipelineMap(context);
539
+ if (!pipelineMap) return null;
540
+
541
+ const entry = pipelineMap[serviceName];
542
+ if (!entry) return null;
543
+
544
+ const pipeline = selectPipelineFromEntry(entry, environment);
545
+ if (!pipeline?.pipelineId) return null;
546
+
547
+ return {
548
+ source: 'local-map',
549
+ serviceName,
550
+ environment: environment || inferEnvironmentFromPipelineName(pipeline.pipelineName),
551
+ pipelineId: String(pipeline.pipelineId),
552
+ pipelineName: pipeline.pipelineName || null
553
+ };
554
+ }
555
+
556
+ function readLocalPipelineMap(context) {
557
+ if (context?.pipelineMap && typeof context.pipelineMap === 'object') {
558
+ return context.pipelineMap;
559
+ }
560
+
561
+ for (const candidate of LOCAL_PIPELINE_MAP_CANDIDATES) {
562
+ if (!candidate || !fs.existsSync(candidate)) continue;
563
+ try {
564
+ return JSON.parse(fs.readFileSync(candidate, 'utf8'));
565
+ } catch (error) {
566
+ continue;
567
+ }
568
+ }
569
+
570
+ return null;
571
+ }
572
+
573
+ function selectPipelineFromEntry(entry, environment) {
574
+ const preferredKeys = getEnvLookupOrder(environment);
575
+ for (const key of preferredKeys) {
576
+ const candidate = normalizePipelineCollection(entry?.[key]);
577
+ if (candidate.length > 0) {
578
+ return candidate[0];
579
+ }
580
+ }
581
+
582
+ for (const value of Object.values(entry || {})) {
583
+ const candidate = normalizePipelineCollection(value);
584
+ if (candidate.length > 0) {
585
+ return candidate[0];
586
+ }
587
+ }
588
+
589
+ return null;
590
+ }
591
+
592
+ function normalizePipelineCollection(value) {
593
+ if (!value) return [];
594
+ const items = Array.isArray(value) ? value : [value];
595
+ return items
596
+ .map((item) => {
597
+ if (!item) return null;
598
+ if (typeof item === 'string') {
599
+ return parsePipelineDescriptor(item);
600
+ }
601
+ if (typeof item === 'object' && item.pipelineId) {
602
+ return {
603
+ pipelineName: item.pipelineName || item.name || null,
604
+ pipelineId: String(item.pipelineId)
605
+ };
606
+ }
607
+ return null;
608
+ })
609
+ .filter(Boolean);
610
+ }
611
+
612
+ async function resolveRemotePipelineForService(client, serviceName, environment) {
613
+ const iterateName = await lookupCurrentIterateName(client);
614
+ const searchers = [
615
+ async () => parseRemotePipelineLookup(
616
+ await client.listDeployChecklists({ pageNum: '1', pageSize: '200', iterate: iterateName }),
617
+ serviceName,
618
+ environment,
619
+ 'deploy-checklists'
620
+ ),
621
+ async () => parseRemotePipelineLookup(
622
+ await client.orderList({ pageNum: '1', pageSize: '200', iterateName }),
623
+ serviceName,
624
+ environment,
625
+ 'order-list'
626
+ )
627
+ ];
628
+
629
+ for (const search of searchers) {
630
+ const resolution = await search();
631
+ if (resolution?.pipelineId) {
632
+ return resolution;
633
+ }
634
+ }
635
+
636
+ return null;
637
+ }
638
+
639
+ async function lookupCurrentIterateName(client) {
640
+ const payload = unwrapMcpPayload(await client.getCurrentIterate());
641
+ if (payload?.name && typeof payload.name === 'string') {
642
+ return payload.name;
643
+ }
644
+ const values = collectNestedValues(payload);
645
+ const iterateName = values.find((value) => typeof value === 'string' && /\d+\.\d+/.test(value));
646
+ return iterateName || null;
647
+ }
648
+
649
+ function parseRemotePipelineLookup(payload, serviceName, environment, source) {
650
+ const records = extractRecordArray(payload);
651
+ for (const record of records) {
652
+ const fields = parseFields(record.fields);
653
+ if (!fields || !matchesRemoteService(fields, serviceName, environment)) {
654
+ continue;
655
+ }
656
+
657
+ const pipeline = selectPipelineFromFields(fields, environment, serviceName);
658
+ if (pipeline?.pipelineId) {
659
+ return {
660
+ source,
661
+ serviceName,
662
+ environment: environment || inferEnvironmentFromPipelineName(pipeline.pipelineName),
663
+ pipelineId: String(pipeline.pipelineId),
664
+ pipelineName: pipeline.pipelineName || null
665
+ };
666
+ }
667
+ }
668
+
669
+ return null;
670
+ }
671
+
672
+ function extractRecordArray(payload) {
673
+ const normalized = unwrapMcpPayload(payload);
674
+ if (!normalized) return [];
675
+ if (Array.isArray(normalized)) return normalized;
676
+
677
+ const candidates = [
678
+ normalized.data,
679
+ normalized.rows,
680
+ normalized.list,
681
+ normalized.records,
682
+ normalized.result?.data,
683
+ normalized.result?.rows,
684
+ normalized.result?.list,
685
+ normalized.result?.records
686
+ ];
687
+
688
+ for (const candidate of candidates) {
689
+ if (Array.isArray(candidate)) {
690
+ return candidate;
691
+ }
692
+ }
693
+
694
+ return [];
695
+ }
696
+
697
+ function parseFields(fields) {
698
+ if (!fields) return null;
699
+ if (typeof fields === 'object') return fields;
700
+ if (typeof fields !== 'string') return null;
701
+
702
+ try {
703
+ return JSON.parse(fields);
704
+ } catch (error) {
705
+ return null;
706
+ }
707
+ }
708
+
709
+ function unwrapMcpPayload(payload) {
710
+ if (!payload || typeof payload !== 'object') {
711
+ return payload;
712
+ }
713
+
714
+ const textContent = Array.isArray(payload.content) ? payload.content : payload.result?.content;
715
+ if (Array.isArray(textContent)) {
716
+ const textItem = textContent.find((item) => item?.type === 'text' && typeof item.text === 'string');
717
+ if (textItem?.text) {
718
+ try {
719
+ return JSON.parse(textItem.text);
720
+ } catch (error) {
721
+ return payload;
722
+ }
723
+ }
724
+ }
725
+
726
+ return payload;
727
+ }
728
+
729
+ function matchesRemoteService(fields, serviceName, environment) {
730
+ const services = Array.isArray(fields.service) ? fields.service.map((item) => String(item).trim()) : [];
731
+ if (services.includes(serviceName)) {
732
+ return true;
733
+ }
734
+
735
+ const expectedPipelineName = buildPrefixedPipelineName(serviceName, environment);
736
+ const pipelineCandidates = collectFieldPipelineDescriptors(fields, environment)
737
+ .map((item) => item.pipelineName)
738
+ .filter(Boolean);
739
+
740
+ return expectedPipelineName ? pipelineCandidates.includes(expectedPipelineName) : false;
741
+ }
742
+
743
+ function selectPipelineFromFields(fields, environment, serviceName) {
744
+ const indexedCandidate = selectIndexedPipelineForService(fields, environment, serviceName);
745
+ if (indexedCandidate?.pipelineId) {
746
+ return indexedCandidate;
747
+ }
748
+
749
+ const candidates = collectFieldPipelineDescriptors(fields, environment);
750
+ if (candidates.length > 0) {
751
+ return candidates[0];
752
+ }
753
+
754
+ const pipelineIds = String(fields.pipelineId || '')
755
+ .split(',')
756
+ .map((item) => item.trim())
757
+ .filter(Boolean);
758
+
759
+ if (pipelineIds.length === 0) {
760
+ return null;
761
+ }
762
+
763
+ return {
764
+ pipelineName: buildPrefixedPipelineName(
765
+ Array.isArray(fields.service) ? fields.service[0] : null,
766
+ environment
767
+ ),
768
+ pipelineId: pipelineIds[0]
769
+ };
770
+ }
771
+
772
+ function selectIndexedPipelineForService(fields, environment, serviceName) {
773
+ if (!serviceName) return null;
774
+
775
+ const services = Array.isArray(fields.service) ? fields.service.map((item) => String(item).trim()) : [];
776
+ const serviceIndex = services.indexOf(serviceName);
777
+ if (serviceIndex < 0) return null;
778
+
779
+ for (const key of getFieldKeysForEnvironment(environment)) {
780
+ const values = Array.isArray(fields[key]) ? fields[key] : [];
781
+ const parsed = parsePipelineDescriptor(values[serviceIndex]);
782
+ if (parsed?.pipelineId) {
783
+ return parsed;
784
+ }
785
+ }
786
+
787
+ return null;
788
+ }
789
+
790
+ function collectFieldPipelineDescriptors(fields, environment) {
791
+ const keys = getFieldKeysForEnvironment(environment);
792
+ const results = [];
793
+
794
+ for (const key of keys) {
795
+ const values = Array.isArray(fields[key]) ? fields[key] : (fields[key] ? [fields[key]] : []);
796
+ for (const value of values) {
797
+ const parsed = parsePipelineDescriptor(value);
798
+ if (parsed?.pipelineId) {
799
+ results.push(parsed);
800
+ }
801
+ }
802
+ }
803
+
804
+ return results;
805
+ }
806
+
807
+ function getFieldKeysForEnvironment(environment) {
808
+ const normalized = normalizeEnvironment(environment || '');
809
+ if (normalized === 'prod') return ['prodPipeline', 'pipeline'];
810
+ if (normalized === 'gray') return ['grayPipeline', 'prodPipeline', 'pipeline'];
811
+ return ['pipeline', 'grayPipeline', 'prodPipeline'];
812
+ }
813
+
814
+ function parsePipelineDescriptor(value) {
815
+ if (!value) return null;
816
+ const parts = String(value).split('#@').map((item) => item.trim()).filter(Boolean);
817
+ if (parts.length < 2) return null;
818
+ return {
819
+ pipelineName: parts[0] || null,
820
+ pipelineId: parts[1] || null
821
+ };
822
+ }
823
+
824
+ function getEnvLookupOrder(environment) {
825
+ const normalized = normalizeEnvironment(environment || '');
826
+ if (normalized === 'prod') return ['prod'];
827
+ if (normalized === 'gray') return ['gray', 'prod'];
828
+ if (normalized === 'uat') return ['uat'];
829
+ if (normalized === 'test') return ['test', 'dev', 'uat'];
830
+ return ['uat', 'test', 'dev', 'gray', 'prod'];
831
+ }
832
+
833
+ function buildPrefixedPipelineName(serviceName, environment) {
834
+ if (!serviceName) return null;
835
+ const normalized = normalizeEnvironment(environment || '');
836
+ if (!normalized) return null;
837
+ const prefix = normalized === 'test' ? 'test' : normalized;
838
+ return `${prefix}-${serviceName}`;
839
+ }
840
+
841
+ function inferEnvironmentFromPipelineName(pipelineName) {
842
+ const match = String(pipelineName || '').match(/^(test|dev|uat|gray|prod)-/i);
843
+ return match ? (ENV_PREFIX_TO_ENV[match[1].toLowerCase()] || normalizeEnvironment(match[1])) : null;
844
+ }
845
+
846
+ function collectNestedValues(payload, values = []) {
847
+ if (payload == null) {
848
+ return values;
849
+ }
850
+
851
+ if (Array.isArray(payload)) {
852
+ for (const item of payload) {
853
+ collectNestedValues(item, values);
854
+ }
855
+ return values;
856
+ }
857
+
858
+ if (typeof payload === 'object') {
859
+ for (const value of Object.values(payload)) {
860
+ collectNestedValues(value, values);
861
+ }
862
+ return values;
863
+ }
864
+
865
+ values.push(payload);
866
+ return values;
867
+ }
868
+
397
869
  function compactObject(value) {
398
870
  return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== null && item !== ''));
399
871
  }
872
+
873
+ function getWorkflowMcpServer(workflow) {
874
+ return workflow.client?.mcpServer || workflow.binding?.mcpServer || workflow.provider;
875
+ }