flowmind 1.5.2 → 1.5.4

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,11 @@
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
+ const { callMcpTool } = require('../../core/mcp-http-client');
10
+
6
11
  const BUILTIN_WORKFLOWS = {
7
12
  'dev-workflow': {
8
13
  name: 'Development Workflow',
@@ -39,6 +44,21 @@ const TOOL_NAMES = {
39
44
  listPipelineRuns: 'flowListPipelineRuns'
40
45
  };
41
46
 
47
+ const LOCAL_PIPELINE_MAP_CANDIDATES = [
48
+ process.env.FLOWMIND_AUTO_FLOW_MAP,
49
+ path.join(process.env.FLOWMIND_HOME || process.env.HOME || process.env.USERPROFILE || os.homedir(), '.flowmind', 'source', 'auto-flow-pipeline-map.json'),
50
+ path.join(process.cwd(), 'source', 'auto-flow-pipeline-map.json'),
51
+ path.join(__dirname, '..', '..', '..', 'source', 'auto-flow-pipeline-map.json')
52
+ ].filter(Boolean);
53
+
54
+ const ENV_PREFIX_TO_ENV = {
55
+ test: 'test',
56
+ dev: 'test',
57
+ uat: 'uat',
58
+ gray: 'gray',
59
+ prod: 'prod'
60
+ };
61
+
42
62
  module.exports = {
43
63
  canHandle(input) {
44
64
  if (!input) return false;
@@ -53,19 +73,14 @@ module.exports = {
53
73
  const workflow = createWorkflowClient(context);
54
74
 
55
75
  if (params.action === 'define') {
76
+ const definition = buildWorkflowDefinitionSummary();
56
77
  return {
57
78
  type: 'result',
58
79
  skill: 'auto-flow',
59
80
  message: 'Define a workflow in YAML format',
60
81
  data: {
61
82
  format: 'YAML workflow definition',
62
- example: {
63
- name: 'My Workflow',
64
- steps: [
65
- { id: 'step1', action: 'run-command', command: 'echo hello' },
66
- { id: 'step2', skill: 'code-review', depends_on: ['step1'] }
67
- ]
68
- }
83
+ execution: definition
69
84
  },
70
85
  input,
71
86
  timestamp: new Date().toISOString()
@@ -77,11 +92,26 @@ module.exports = {
77
92
  }
78
93
 
79
94
  if (params.action === 'status') {
80
- return executeStatus(workflow, params, input);
95
+ const execution = await executeStatus(workflow, params, input);
96
+ return {
97
+ type: 'result',
98
+ skill: 'auto-flow',
99
+ message: 'Workflow status: query completed',
100
+ data: {
101
+ action: 'status',
102
+ provider: workflow.provider,
103
+ binding: workflow.binding,
104
+ status: 'completed',
105
+ mcpServer: getWorkflowMcpServer(workflow),
106
+ execution: summarizeWorkflowExecution('status', params, execution)
107
+ },
108
+ input,
109
+ timestamp: new Date().toISOString()
110
+ };
81
111
  }
82
112
 
83
113
  if (params.action === 'deploy' || params.action === 'run') {
84
- return executeDeploy(workflow, params, input);
114
+ return executeDeploy(workflow, params, input, context);
85
115
  }
86
116
 
87
117
  if (params.action === 'list' || params.serviceNames.length > 0) {
@@ -89,15 +119,14 @@ module.exports = {
89
119
  return {
90
120
  type: 'result',
91
121
  skill: 'auto-flow',
92
- message: params.serviceNames.length > 0
93
- ? `Resolved workflow query for: ${params.serviceNames.join(', ')}`
94
- : 'Listing available deployment pipelines',
122
+ message: 'Workflow status: pipeline lookup completed',
95
123
  data: {
96
124
  action: 'list',
97
125
  provider: workflow.provider,
98
126
  binding: workflow.binding,
99
- filters: buildListPipelineParams(params),
100
- execution
127
+ status: 'completed',
128
+ mcpServer: getWorkflowMcpServer(workflow),
129
+ execution: summarizeWorkflowExecution('list', params, execution)
101
130
  },
102
131
  input,
103
132
  timestamp: new Date().toISOString()
@@ -112,7 +141,7 @@ module.exports = {
112
141
  message: `Workflow ready: ${workflowDef.name}`,
113
142
  data: {
114
143
  workflow: params.workflow,
115
- steps: workflowDef.steps,
144
+ execution: buildBuiltinWorkflowSummary(params.workflow, workflowDef),
116
145
  provider: workflow.provider
117
146
  },
118
147
  input,
@@ -133,7 +162,8 @@ module.exports = {
133
162
  ],
134
163
  builtinWorkflows: Object.keys(BUILTIN_WORKFLOWS),
135
164
  provider: workflow.provider,
136
- binding: workflow.binding
165
+ binding: workflow.binding,
166
+ execution: buildAutoFlowHintSummary(workflow)
137
167
  },
138
168
  input,
139
169
  timestamp: new Date().toISOString()
@@ -147,7 +177,8 @@ function createWorkflowClient(context) {
147
177
  return {
148
178
  client: adapter,
149
179
  provider: adapter.providerName,
150
- binding: context.resourceBinding?.componentType === 'workflow' ? context.resourceBinding : null
180
+ binding: context.resourceBinding?.componentType === 'workflow' ? context.resourceBinding : null,
181
+ transportBacked: true
151
182
  };
152
183
  }
153
184
 
@@ -156,33 +187,394 @@ function createWorkflowClient(context) {
156
187
  : null;
157
188
 
158
189
  if (!binding?.mcpServer) {
159
- return { client: null, provider: null, binding: null };
190
+ return { client: null, provider: null, binding: null, transportBacked: false };
191
+ }
192
+
193
+ const transport = buildWorkflowTransport(binding);
194
+ if (!transport.url) {
195
+ return { client: null, provider: binding.provider || null, binding, transportBacked: false };
160
196
  }
161
197
 
162
198
  return {
163
199
  client: {
164
200
  providerName: binding.provider || 'workflow-binding',
165
201
  async listPipelines(params) {
166
- return { mcpServer: binding.mcpServer, tool: TOOL_NAMES.listPipelines, params };
202
+ return callWorkflowMcp(binding, transport, TOOL_NAMES.listPipelines, params);
203
+ },
204
+ async getCurrentIterate() {
205
+ return callWorkflowMcp(binding, transport, 'getCurrentIterate', {});
206
+ },
207
+ async listDeployChecklists(params) {
208
+ return callWorkflowMcp(binding, transport, 'listDeployChecklists', params);
209
+ },
210
+ async orderList(params) {
211
+ return callWorkflowMcp(binding, transport, 'orderList', params);
167
212
  },
168
213
  async startPipelineRun(pipelineId) {
169
- return { mcpServer: binding.mcpServer, tool: TOOL_NAMES.startPipelineRun, params: { pipelineId } };
214
+ return callWorkflowMcp(binding, transport, TOOL_NAMES.startPipelineRun, { pipelineId });
170
215
  },
171
216
  async startBatchPipelineRun(params) {
172
- return { mcpServer: binding.mcpServer, tool: TOOL_NAMES.startBatchPipelineRun, params };
217
+ return callWorkflowMcp(binding, transport, TOOL_NAMES.startBatchPipelineRun, params);
173
218
  },
174
219
  async getPipelineRun(pipelineId, runId) {
175
- return { mcpServer: binding.mcpServer, tool: TOOL_NAMES.getPipelineRun, params: { pipelineId, pipelineRunId: runId } };
220
+ return callWorkflowMcp(binding, transport, TOOL_NAMES.getPipelineRun, { pipelineId, pipelineRunId: runId });
176
221
  },
177
222
  async listPipelineRuns(pipelineId, params) {
178
- return { mcpServer: binding.mcpServer, tool: TOOL_NAMES.listPipelineRuns, params: { pipelineId, ...(params || {}) } };
223
+ return callWorkflowMcp(binding, transport, TOOL_NAMES.listPipelineRuns, { pipelineId, ...(params || {}) });
179
224
  }
180
225
  },
181
226
  provider: binding.provider || 'workflow-binding',
182
- binding
227
+ binding,
228
+ transportBacked: true
229
+ };
230
+ }
231
+
232
+ function buildWorkflowTransport(binding) {
233
+ const connection = binding?.connection || {};
234
+ const headers = {};
235
+
236
+ if (connection.token) {
237
+ headers.Authorization = `Bearer ${connection.token}`;
238
+ }
239
+ if (connection.apiKey) {
240
+ headers['X-API-Key'] = connection.apiKey;
241
+ }
242
+
243
+ return {
244
+ url: connection.url || connection.address || connection.endpoint || null,
245
+ headers,
246
+ timeoutMs: connection.timeoutMs || 60000
183
247
  };
184
248
  }
185
249
 
250
+ function callWorkflowMcp(binding, transport, tool, params) {
251
+ return callMcpTool({
252
+ url: transport.url,
253
+ headers: transport.headers,
254
+ tool,
255
+ args: params || {},
256
+ timeoutMs: transport.timeoutMs || 60000,
257
+ serverName: binding.mcpServer || binding.provider || 'workflow'
258
+ });
259
+ }
260
+
261
+ function summarizeWorkflowExecution(action, params, execution) {
262
+ const payload = unwrapMcpResult(execution);
263
+ const records = extractRecords(payload);
264
+ const total = extractTotalCount(payload, records);
265
+
266
+ return compactObject({
267
+ action,
268
+ summary: buildWorkflowSummaryText(action, params, total, records),
269
+ total,
270
+ status: extractWorkflowStatus(payload, action),
271
+ target: buildWorkflowTargetLabel(params),
272
+ environment: params.environment || null,
273
+ items: records.slice(0, 5).map((record) => summarizeWorkflowRecord(record))
274
+ });
275
+ }
276
+
277
+ function summarizeDeployExecution(params, execution, context = {}) {
278
+ const payload = unwrapMcpResult(execution);
279
+ const payloadList = Array.isArray(payload) ? payload : [payload];
280
+ const primary = payloadList.find((item) => item !== null && item !== undefined) || null;
281
+ const summaryTarget = buildWorkflowTargetLabel(params) || buildDeployTargetLabel(context) || 'deployment';
282
+
283
+ return compactObject({
284
+ action: 'deploy',
285
+ summary: buildDeploySummaryText(summaryTarget, primary, context, payloadList.length),
286
+ status: extractDeployStatus(primary),
287
+ target: summaryTarget,
288
+ pipelineId: context.pipelineId || params.pipelineId || (Array.isArray(context.pipelineIds) && context.pipelineIds.length === 1 ? context.pipelineIds[0] : null),
289
+ pipelineIds: Array.isArray(context.pipelineIds) && context.pipelineIds.length > 0 ? context.pipelineIds : null,
290
+ source: context.source || null,
291
+ runId: extractDeployRunId(primary),
292
+ count: payloadList.length > 1 ? payloadList.length : null,
293
+ details: summarizeDeployDetails(primary)
294
+ });
295
+ }
296
+
297
+ function buildWorkflowDefinitionSummary() {
298
+ return {
299
+ action: 'define',
300
+ summary: 'Prepared a YAML workflow template',
301
+ format: 'YAML workflow definition',
302
+ example: {
303
+ name: 'My Workflow',
304
+ steps: [
305
+ { id: 'step1', action: 'run-command', command: 'echo hello' },
306
+ { id: 'step2', skill: 'code-review', depends_on: ['step1'] }
307
+ ]
308
+ }
309
+ };
310
+ }
311
+
312
+ function buildBuiltinWorkflowSummary(workflowName, workflowDef) {
313
+ return {
314
+ action: 'workflow',
315
+ summary: `Workflow ready: ${workflowDef.name}`,
316
+ workflow: workflowName,
317
+ name: workflowDef.name,
318
+ stepCount: Array.isArray(workflowDef.steps) ? workflowDef.steps.length : 0,
319
+ steps: (workflowDef.steps || []).map((step) => compactObject({
320
+ id: step.id,
321
+ skill: step.skill || null,
322
+ action: step.action || null,
323
+ name: step.name || null,
324
+ depends_on: Array.isArray(step.depends_on) ? step.depends_on : null
325
+ }))
326
+ };
327
+ }
328
+
329
+ function buildAutoFlowHintSummary(workflow) {
330
+ return {
331
+ action: 'hint',
332
+ summary: workflow?.binding
333
+ ? `Workflow service bound to ${workflow.binding.mcpServer}`
334
+ : 'Workflow service not configured',
335
+ provider: workflow?.provider || null,
336
+ binding: workflow?.binding || null,
337
+ configured: !!workflow?.client
338
+ };
339
+ }
340
+
341
+ function buildWorkflowSummaryText(action, params, total, records) {
342
+ const target = buildWorkflowTargetLabel(params) || 'all workflows';
343
+ const count = typeof total === 'number' ? total : records.length;
344
+
345
+ if (action === 'status') {
346
+ return `Checked ${target}${count !== null ? `, ${count} record(s) found` : ''}`;
347
+ }
348
+
349
+ if (action === 'list') {
350
+ return `Listed workflows for ${target}${count !== null ? `, ${count} pipeline(s) found` : ''}`;
351
+ }
352
+
353
+ return `Workflow ${action} completed`;
354
+ }
355
+
356
+ function buildWorkflowTargetLabel(params) {
357
+ if (params.pipelineId && params.runId) {
358
+ return `pipeline ${params.pipelineId} / run ${params.runId}`;
359
+ }
360
+ if (params.pipelineId) {
361
+ return `pipeline ${params.pipelineId}`;
362
+ }
363
+ if (params.serviceNames?.length > 0) {
364
+ return params.serviceNames.join(', ');
365
+ }
366
+ if (params.workflow) {
367
+ return params.workflow;
368
+ }
369
+ return params.environment || null;
370
+ }
371
+
372
+ function buildDeployTargetLabel(context) {
373
+ if (Array.isArray(context.pipelineIds) && context.pipelineIds.length > 0) {
374
+ return `pipeline ${context.pipelineIds.join(', ')}`;
375
+ }
376
+ if (context.pipelineId) {
377
+ return `pipeline ${context.pipelineId}`;
378
+ }
379
+ if (Array.isArray(context.services) && context.services.length > 0) {
380
+ return context.services.map((item) => item.serviceName || item.service || item.pipelineName || item.pipelineId).filter(Boolean).join(', ');
381
+ }
382
+ return null;
383
+ }
384
+
385
+ function buildDeploySummaryText(target, payload, context, count = 1) {
386
+ const status = extractDeployStatus(payload) || 'submitted';
387
+ const runId = extractDeployRunId(payload);
388
+ const source = context.source ? ` via ${context.source}` : '';
389
+ const countSuffix = count > 1 ? ` (${count} items)` : '';
390
+
391
+ if (runId) {
392
+ return `Deployment ${status} for ${target}${source}, run ${runId}${countSuffix}`;
393
+ }
394
+
395
+ return `Deployment ${status} for ${target}${source}${countSuffix}`;
396
+ }
397
+
398
+ function extractDeployStatus(payload) {
399
+ if (payload && typeof payload === 'object') {
400
+ const candidates = [
401
+ payload.status,
402
+ payload.state,
403
+ payload.result?.status,
404
+ payload.result?.state
405
+ ].filter(Boolean);
406
+ if (candidates.length > 0) {
407
+ return String(candidates[0]);
408
+ }
409
+
410
+ if (payload.submitted === true) {
411
+ return 'submitted';
412
+ }
413
+ }
414
+
415
+ return null;
416
+ }
417
+
418
+ function extractDeployRunId(payload) {
419
+ if (!payload || typeof payload !== 'object') {
420
+ return null;
421
+ }
422
+
423
+ const candidates = [
424
+ payload.pipelineRunId,
425
+ payload.pipeline_run_id,
426
+ payload.runId,
427
+ payload.run_id,
428
+ payload.result?.pipelineRunId,
429
+ payload.result?.pipeline_run_id,
430
+ payload.result?.runId,
431
+ payload.result?.run_id
432
+ ];
433
+
434
+ for (const candidate of candidates) {
435
+ if (candidate === null || candidate === undefined || candidate === '') continue;
436
+ return String(candidate);
437
+ }
438
+
439
+ return null;
440
+ }
441
+
442
+ function summarizeDeployDetails(payload) {
443
+ if (!payload || typeof payload !== 'object') {
444
+ return null;
445
+ }
446
+
447
+ const details = compactObject({
448
+ submitted: typeof payload.submitted === 'boolean' ? payload.submitted : null,
449
+ pipelineRunId: payload.pipelineRunId || payload.pipeline_run_id || null,
450
+ pipelineId: payload.pipelineId || payload.pipeline_id || null,
451
+ message: payload.message || payload.result?.message || null
452
+ });
453
+
454
+ return Object.keys(details).length > 0 ? details : null;
455
+ }
456
+
457
+ function unwrapMcpResult(payload) {
458
+ if (!payload || typeof payload !== 'object') {
459
+ return payload;
460
+ }
461
+
462
+ const textContent = Array.isArray(payload.content) ? payload.content : payload.result?.content;
463
+ if (Array.isArray(textContent)) {
464
+ const textItem = textContent.find((item) => item?.type === 'text' && typeof item.text === 'string');
465
+ if (textItem?.text) {
466
+ try {
467
+ return JSON.parse(textItem.text);
468
+ } catch (error) {
469
+ return payload;
470
+ }
471
+ }
472
+ }
473
+
474
+ return payload;
475
+ }
476
+
477
+ function extractRecords(payload) {
478
+ const normalized = unwrapMcpResult(payload);
479
+ if (!normalized) return [];
480
+ if (Array.isArray(normalized)) return normalized;
481
+
482
+ const candidates = [
483
+ normalized.data,
484
+ normalized.rows,
485
+ normalized.list,
486
+ normalized.records,
487
+ normalized.result?.data,
488
+ normalized.result?.rows,
489
+ normalized.result?.list,
490
+ normalized.result?.records
491
+ ];
492
+
493
+ for (const candidate of candidates) {
494
+ if (Array.isArray(candidate)) {
495
+ return candidate;
496
+ }
497
+ }
498
+
499
+ return [];
500
+ }
501
+
502
+ function extractTotalCount(payload, records) {
503
+ if (payload && typeof payload === 'object') {
504
+ const candidates = [payload.total, payload.count, payload.result?.total, payload.result?.count];
505
+ for (const candidate of candidates) {
506
+ if (typeof candidate === 'number' && Number.isFinite(candidate)) {
507
+ return candidate;
508
+ }
509
+ if (typeof candidate === 'string' && candidate.trim() !== '' && !Number.isNaN(Number(candidate))) {
510
+ return Number(candidate);
511
+ }
512
+ }
513
+ }
514
+
515
+ return records.length;
516
+ }
517
+
518
+ function extractWorkflowStatus(payload, action) {
519
+ if (payload && typeof payload === 'object') {
520
+ const candidates = [
521
+ payload.status,
522
+ payload.state,
523
+ payload.result?.status,
524
+ payload.result?.state
525
+ ].filter(Boolean);
526
+ if (candidates.length > 0) {
527
+ return String(candidates[0]);
528
+ }
529
+ }
530
+
531
+ return action === 'status' ? 'completed' : 'completed';
532
+ }
533
+
534
+ function summarizeWorkflowRecord(record) {
535
+ const fields = parseWorkflowFields(record?.fields);
536
+ return compactObject({
537
+ pipelineName: record?.pipelineName || record?.name || fields?.pipelineName || fields?.name || null,
538
+ pipelineId: record?.pipelineId || fields?.pipelineId || extractIdFromFields(fields) || null,
539
+ service: extractWorkflowService(fields),
540
+ status: record?.status || fields?.status || fields?.state || null,
541
+ env: record?.env || fields?.env || fields?.environment || null
542
+ });
543
+ }
544
+
545
+ function parseWorkflowFields(fields) {
546
+ if (!fields) return null;
547
+ if (typeof fields === 'object') return fields;
548
+ if (typeof fields !== 'string') return null;
549
+
550
+ try {
551
+ return JSON.parse(fields);
552
+ } catch (error) {
553
+ return null;
554
+ }
555
+ }
556
+
557
+ function extractWorkflowService(fields) {
558
+ if (!fields) return null;
559
+ if (Array.isArray(fields.service) && fields.service.length > 0) {
560
+ return fields.service[0];
561
+ }
562
+ if (typeof fields.service === 'string') {
563
+ return fields.service;
564
+ }
565
+ return fields.serviceName || fields.service_name || null;
566
+ }
567
+
568
+ function extractIdFromFields(fields) {
569
+ if (!fields || typeof fields !== 'object') return null;
570
+ const candidates = [fields.pipelineId, fields.pipeline_id, fields.id];
571
+ for (const candidate of candidates) {
572
+ if (candidate === null || candidate === undefined || candidate === '') continue;
573
+ return String(candidate);
574
+ }
575
+ return null;
576
+ }
577
+
186
578
  function buildNoAdapterResult(input, params) {
187
579
  if (params.workflow && BUILTIN_WORKFLOWS[params.workflow]) {
188
580
  const workflow = BUILTIN_WORKFLOWS[params.workflow];
@@ -192,7 +584,7 @@ function buildNoAdapterResult(input, params) {
192
584
  message: `Workflow ready: ${workflow.name}`,
193
585
  data: {
194
586
  workflow: params.workflow,
195
- steps: workflow.steps
587
+ execution: buildBuiltinWorkflowSummary(params.workflow, workflow)
196
588
  },
197
589
  input,
198
590
  timestamp: new Date().toISOString()
@@ -204,28 +596,73 @@ function buildNoAdapterResult(input, params) {
204
596
  skill: 'auto-flow',
205
597
  message: 'Workflow service not configured. Connect friday-auto-flow first.',
206
598
  data: {
207
- params,
208
- hint: 'Run `flowmind resource` to review current bindings, then save one like: `flowmind "绑定发布业务 mcp=friday-auto-flow token=xxx env=uat"`'
599
+ status: 'not_configured',
600
+ hint: 'Run `flowmind resource` to review current bindings, then save one like: `flowmind "绑定发布业务 mcp=friday-auto-flow token=xxx env=uat"`',
601
+ execution: buildAutoFlowHintSummary({
602
+ client: null,
603
+ provider: null,
604
+ binding: null
605
+ })
209
606
  },
210
607
  input,
211
608
  timestamp: new Date().toISOString()
212
609
  };
213
610
  }
214
611
 
215
- async function executeDeploy(workflow, params, input) {
612
+ async function executeDeploy(workflow, params, input, context) {
216
613
  if (params.pipelineId) {
217
614
  const execution = await workflow.client.startPipelineRun(params.pipelineId);
218
615
  return {
219
616
  type: 'result',
220
617
  skill: 'auto-flow',
221
- message: `Starting pipeline ${params.pipelineId}`,
618
+ message: 'Workflow status: deployment submitted',
222
619
  data: {
223
620
  action: 'deploy',
224
- pipelineId: params.pipelineId,
225
- environment: params.environment,
226
621
  provider: workflow.provider,
227
622
  binding: workflow.binding,
228
- execution
623
+ status: 'submitted',
624
+ mcpServer: getWorkflowMcpServer(workflow),
625
+ execution: summarizeDeployExecution(params, execution, {
626
+ source: 'direct-input',
627
+ pipelineId: params.pipelineId
628
+ }),
629
+ resolution: {
630
+ source: 'direct-input',
631
+ pipelineId: params.pipelineId
632
+ }
633
+ },
634
+ input,
635
+ timestamp: new Date().toISOString()
636
+ };
637
+ }
638
+
639
+ const resolvedTargets = await resolvePipelineTargets(workflow, params, context);
640
+ if (resolvedTargets.pipelineIds.length > 0) {
641
+ const executions = [];
642
+ for (const pipelineId of resolvedTargets.pipelineIds) {
643
+ executions.push(await workflow.client.startPipelineRun(pipelineId));
644
+ }
645
+
646
+ return {
647
+ type: 'result',
648
+ skill: 'auto-flow',
649
+ message: 'Workflow status: deployment submitted',
650
+ data: {
651
+ action: 'deploy',
652
+ provider: workflow.provider,
653
+ binding: workflow.binding,
654
+ status: 'submitted',
655
+ mcpServer: getWorkflowMcpServer(workflow),
656
+ execution: summarizeDeployExecution(params, executions.length === 1 ? executions[0] : executions, {
657
+ source: resolvedTargets.source,
658
+ pipelineIds: resolvedTargets.pipelineIds,
659
+ services: resolvedTargets.resolutions
660
+ }),
661
+ resolution: {
662
+ source: resolvedTargets.source,
663
+ pipelineIds: resolvedTargets.pipelineIds,
664
+ services: resolvedTargets.resolutions
665
+ }
229
666
  },
230
667
  input,
231
668
  timestamp: new Date().toISOString()
@@ -233,33 +670,33 @@ async function executeDeploy(workflow, params, input) {
233
670
  }
234
671
 
235
672
  const listParams = buildListPipelineParams(params);
236
- const resolveExecution = await workflow.client.listPipelines(listParams);
673
+ const lookup = await workflow.client.listPipelines(listParams);
237
674
  const batchParams = buildBatchRunParams(params);
238
675
  const execution = await workflow.client.startBatchPipelineRun(batchParams);
239
676
 
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
677
+ return {
678
+ type: 'result',
679
+ skill: 'auto-flow',
680
+ message: 'Workflow status: deployment submitted',
681
+ data: {
682
+ action: 'deploy',
683
+ provider: workflow.provider,
684
+ binding: workflow.binding,
685
+ status: 'submitted',
686
+ mcpServer: getWorkflowMcpServer(workflow),
687
+ execution: summarizeDeployExecution(params, execution, {
688
+ source: 'batch-fallback',
689
+ lookup
690
+ }),
691
+ resolution: {
692
+ source: 'batch-fallback',
693
+ lookup
694
+ }
256
695
  },
257
- execution
258
- },
259
- input,
260
- timestamp: new Date().toISOString()
261
- };
262
- }
696
+ input,
697
+ timestamp: new Date().toISOString()
698
+ };
699
+ }
263
700
 
264
701
  async function executeStatus(workflow, params, input) {
265
702
  let execution;
@@ -273,25 +710,7 @@ async function executeStatus(workflow, params, input) {
273
710
  execution = await workflow.client.listPipelines(buildListPipelineParams(params));
274
711
  }
275
712
 
276
- return {
277
- type: 'result',
278
- skill: 'auto-flow',
279
- message: params.runId
280
- ? `Querying run status for ${params.runId}`
281
- : 'Querying pipeline status',
282
- data: {
283
- action: 'status',
284
- services: params.serviceNames,
285
- pipelineId: params.pipelineId,
286
- runId: params.runId,
287
- environment: params.environment,
288
- provider: workflow.provider,
289
- binding: workflow.binding,
290
- execution
291
- },
292
- input,
293
- timestamp: new Date().toISOString()
294
- };
713
+ return execution;
295
714
  }
296
715
 
297
716
  function parseFlowParams(input) {
@@ -337,6 +756,7 @@ function parseFlowParams(input) {
337
756
  }
338
757
 
339
758
  params.serviceNames = extractServiceNames(input);
759
+ applyPrefixedServiceNormalization(params);
340
760
 
341
761
  if (!params.workflow && /deploy-workflow|dev-workflow/i.test(input)) {
342
762
  params.workflow = input.match(/deploy-workflow|dev-workflow/i)[0];
@@ -394,6 +814,415 @@ function normalizeEnvironment(value) {
394
814
  return normalized;
395
815
  }
396
816
 
817
+ function applyPrefixedServiceNormalization(params) {
818
+ const normalizedServices = [];
819
+
820
+ for (const originalName of params.serviceNames) {
821
+ const prefixed = splitPrefixedServiceName(originalName);
822
+ if (prefixed) {
823
+ if (!params.environment) {
824
+ params.environment = prefixed.environment;
825
+ }
826
+ normalizedServices.push(prefixed.serviceName);
827
+ continue;
828
+ }
829
+ normalizedServices.push(originalName);
830
+ }
831
+
832
+ params.serviceNames = [...new Set(normalizedServices)];
833
+ }
834
+
835
+ function splitPrefixedServiceName(serviceName) {
836
+ const match = String(serviceName || '').match(/^(test|dev|uat|gray|prod)-(.+)$/i);
837
+ if (!match || !match[2] || !match[2].includes('-')) {
838
+ return null;
839
+ }
840
+
841
+ return {
842
+ environment: ENV_PREFIX_TO_ENV[match[1].toLowerCase()] || normalizeEnvironment(match[1]),
843
+ serviceName: match[2]
844
+ };
845
+ }
846
+
847
+ async function resolvePipelineTargets(workflow, params, context) {
848
+ if (params.serviceNames.length === 0) {
849
+ return { source: null, resolutions: [], pipelineIds: [] };
850
+ }
851
+
852
+ const resolutions = [];
853
+ let source = null;
854
+
855
+ for (const serviceName of params.serviceNames) {
856
+ const resolution = await resolvePipelineForService(workflow, {
857
+ serviceName,
858
+ environment: params.environment,
859
+ context
860
+ });
861
+
862
+ if (!resolution?.pipelineId) {
863
+ return { source: null, resolutions: [], pipelineIds: [] };
864
+ }
865
+
866
+ source = source || resolution.source;
867
+ resolutions.push(resolution);
868
+ }
869
+
870
+ return {
871
+ source,
872
+ resolutions,
873
+ pipelineIds: [...new Set(resolutions.map((item) => String(item.pipelineId)))]
874
+ };
875
+ }
876
+
877
+ async function resolvePipelineForService(workflow, { serviceName, environment, context }) {
878
+ const localResolution = resolveLocalPipelineForService(serviceName, environment, context);
879
+ if (localResolution) {
880
+ return localResolution;
881
+ }
882
+
883
+ if (!workflow.transportBacked || typeof workflow.client?.getCurrentIterate !== 'function') {
884
+ return null;
885
+ }
886
+
887
+ return resolveRemotePipelineForService(workflow.client, serviceName, environment);
888
+ }
889
+
890
+ function resolveLocalPipelineForService(serviceName, environment, context) {
891
+ const pipelineMap = readLocalPipelineMap(context);
892
+ if (!pipelineMap) return null;
893
+
894
+ const entry = pipelineMap[serviceName];
895
+ if (!entry) return null;
896
+
897
+ const pipeline = selectPipelineFromEntry(entry, environment);
898
+ if (!pipeline?.pipelineId) return null;
899
+
900
+ return {
901
+ source: 'local-map',
902
+ serviceName,
903
+ environment: environment || inferEnvironmentFromPipelineName(pipeline.pipelineName),
904
+ pipelineId: String(pipeline.pipelineId),
905
+ pipelineName: pipeline.pipelineName || null
906
+ };
907
+ }
908
+
909
+ function readLocalPipelineMap(context) {
910
+ if (context?.pipelineMap && typeof context.pipelineMap === 'object') {
911
+ return context.pipelineMap;
912
+ }
913
+
914
+ for (const candidate of LOCAL_PIPELINE_MAP_CANDIDATES) {
915
+ if (!candidate || !fs.existsSync(candidate)) continue;
916
+ try {
917
+ return JSON.parse(fs.readFileSync(candidate, 'utf8'));
918
+ } catch (error) {
919
+ continue;
920
+ }
921
+ }
922
+
923
+ return null;
924
+ }
925
+
926
+ function selectPipelineFromEntry(entry, environment) {
927
+ const preferredKeys = getEnvLookupOrder(environment);
928
+ for (const key of preferredKeys) {
929
+ const candidate = normalizePipelineCollection(entry?.[key]);
930
+ if (candidate.length > 0) {
931
+ return candidate[0];
932
+ }
933
+ }
934
+
935
+ for (const value of Object.values(entry || {})) {
936
+ const candidate = normalizePipelineCollection(value);
937
+ if (candidate.length > 0) {
938
+ return candidate[0];
939
+ }
940
+ }
941
+
942
+ return null;
943
+ }
944
+
945
+ function normalizePipelineCollection(value) {
946
+ if (!value) return [];
947
+ const items = Array.isArray(value) ? value : [value];
948
+ return items
949
+ .map((item) => {
950
+ if (!item) return null;
951
+ if (typeof item === 'string') {
952
+ return parsePipelineDescriptor(item);
953
+ }
954
+ if (typeof item === 'object' && item.pipelineId) {
955
+ return {
956
+ pipelineName: item.pipelineName || item.name || null,
957
+ pipelineId: String(item.pipelineId)
958
+ };
959
+ }
960
+ return null;
961
+ })
962
+ .filter(Boolean);
963
+ }
964
+
965
+ async function resolveRemotePipelineForService(client, serviceName, environment) {
966
+ const iterateName = await lookupCurrentIterateName(client);
967
+ const searchers = [
968
+ async () => parseRemotePipelineLookup(
969
+ await client.listDeployChecklists({ pageNum: '1', pageSize: '200', iterate: iterateName }),
970
+ serviceName,
971
+ environment,
972
+ 'deploy-checklists'
973
+ ),
974
+ async () => parseRemotePipelineLookup(
975
+ await client.orderList({ pageNum: '1', pageSize: '200', iterateName }),
976
+ serviceName,
977
+ environment,
978
+ 'order-list'
979
+ )
980
+ ];
981
+
982
+ for (const search of searchers) {
983
+ const resolution = await search();
984
+ if (resolution?.pipelineId) {
985
+ return resolution;
986
+ }
987
+ }
988
+
989
+ return null;
990
+ }
991
+
992
+ async function lookupCurrentIterateName(client) {
993
+ const payload = unwrapMcpPayload(await client.getCurrentIterate());
994
+ if (payload?.name && typeof payload.name === 'string') {
995
+ return payload.name;
996
+ }
997
+ const values = collectNestedValues(payload);
998
+ const iterateName = values.find((value) => typeof value === 'string' && /\d+\.\d+/.test(value));
999
+ return iterateName || null;
1000
+ }
1001
+
1002
+ function parseRemotePipelineLookup(payload, serviceName, environment, source) {
1003
+ const records = extractRecordArray(payload);
1004
+ for (const record of records) {
1005
+ const fields = parseFields(record.fields);
1006
+ if (!fields || !matchesRemoteService(fields, serviceName, environment)) {
1007
+ continue;
1008
+ }
1009
+
1010
+ const pipeline = selectPipelineFromFields(fields, environment, serviceName);
1011
+ if (pipeline?.pipelineId) {
1012
+ return {
1013
+ source,
1014
+ serviceName,
1015
+ environment: environment || inferEnvironmentFromPipelineName(pipeline.pipelineName),
1016
+ pipelineId: String(pipeline.pipelineId),
1017
+ pipelineName: pipeline.pipelineName || null
1018
+ };
1019
+ }
1020
+ }
1021
+
1022
+ return null;
1023
+ }
1024
+
1025
+ function extractRecordArray(payload) {
1026
+ const normalized = unwrapMcpPayload(payload);
1027
+ if (!normalized) return [];
1028
+ if (Array.isArray(normalized)) return normalized;
1029
+
1030
+ const candidates = [
1031
+ normalized.data,
1032
+ normalized.rows,
1033
+ normalized.list,
1034
+ normalized.records,
1035
+ normalized.result?.data,
1036
+ normalized.result?.rows,
1037
+ normalized.result?.list,
1038
+ normalized.result?.records
1039
+ ];
1040
+
1041
+ for (const candidate of candidates) {
1042
+ if (Array.isArray(candidate)) {
1043
+ return candidate;
1044
+ }
1045
+ }
1046
+
1047
+ return [];
1048
+ }
1049
+
1050
+ function parseFields(fields) {
1051
+ if (!fields) return null;
1052
+ if (typeof fields === 'object') return fields;
1053
+ if (typeof fields !== 'string') return null;
1054
+
1055
+ try {
1056
+ return JSON.parse(fields);
1057
+ } catch (error) {
1058
+ return null;
1059
+ }
1060
+ }
1061
+
1062
+ function unwrapMcpPayload(payload) {
1063
+ if (!payload || typeof payload !== 'object') {
1064
+ return payload;
1065
+ }
1066
+
1067
+ const textContent = Array.isArray(payload.content) ? payload.content : payload.result?.content;
1068
+ if (Array.isArray(textContent)) {
1069
+ const textItem = textContent.find((item) => item?.type === 'text' && typeof item.text === 'string');
1070
+ if (textItem?.text) {
1071
+ try {
1072
+ return JSON.parse(textItem.text);
1073
+ } catch (error) {
1074
+ return payload;
1075
+ }
1076
+ }
1077
+ }
1078
+
1079
+ return payload;
1080
+ }
1081
+
1082
+ function matchesRemoteService(fields, serviceName, environment) {
1083
+ const services = Array.isArray(fields.service) ? fields.service.map((item) => String(item).trim()) : [];
1084
+ if (services.includes(serviceName)) {
1085
+ return true;
1086
+ }
1087
+
1088
+ const expectedPipelineName = buildPrefixedPipelineName(serviceName, environment);
1089
+ const pipelineCandidates = collectFieldPipelineDescriptors(fields, environment)
1090
+ .map((item) => item.pipelineName)
1091
+ .filter(Boolean);
1092
+
1093
+ return expectedPipelineName ? pipelineCandidates.includes(expectedPipelineName) : false;
1094
+ }
1095
+
1096
+ function selectPipelineFromFields(fields, environment, serviceName) {
1097
+ const indexedCandidate = selectIndexedPipelineForService(fields, environment, serviceName);
1098
+ if (indexedCandidate?.pipelineId) {
1099
+ return indexedCandidate;
1100
+ }
1101
+
1102
+ const candidates = collectFieldPipelineDescriptors(fields, environment);
1103
+ if (candidates.length > 0) {
1104
+ return candidates[0];
1105
+ }
1106
+
1107
+ const pipelineIds = String(fields.pipelineId || '')
1108
+ .split(',')
1109
+ .map((item) => item.trim())
1110
+ .filter(Boolean);
1111
+
1112
+ if (pipelineIds.length === 0) {
1113
+ return null;
1114
+ }
1115
+
1116
+ return {
1117
+ pipelineName: buildPrefixedPipelineName(
1118
+ Array.isArray(fields.service) ? fields.service[0] : null,
1119
+ environment
1120
+ ),
1121
+ pipelineId: pipelineIds[0]
1122
+ };
1123
+ }
1124
+
1125
+ function selectIndexedPipelineForService(fields, environment, serviceName) {
1126
+ if (!serviceName) return null;
1127
+
1128
+ const services = Array.isArray(fields.service) ? fields.service.map((item) => String(item).trim()) : [];
1129
+ const serviceIndex = services.indexOf(serviceName);
1130
+ if (serviceIndex < 0) return null;
1131
+
1132
+ for (const key of getFieldKeysForEnvironment(environment)) {
1133
+ const values = Array.isArray(fields[key]) ? fields[key] : [];
1134
+ const parsed = parsePipelineDescriptor(values[serviceIndex]);
1135
+ if (parsed?.pipelineId) {
1136
+ return parsed;
1137
+ }
1138
+ }
1139
+
1140
+ return null;
1141
+ }
1142
+
1143
+ function collectFieldPipelineDescriptors(fields, environment) {
1144
+ const keys = getFieldKeysForEnvironment(environment);
1145
+ const results = [];
1146
+
1147
+ for (const key of keys) {
1148
+ const values = Array.isArray(fields[key]) ? fields[key] : (fields[key] ? [fields[key]] : []);
1149
+ for (const value of values) {
1150
+ const parsed = parsePipelineDescriptor(value);
1151
+ if (parsed?.pipelineId) {
1152
+ results.push(parsed);
1153
+ }
1154
+ }
1155
+ }
1156
+
1157
+ return results;
1158
+ }
1159
+
1160
+ function getFieldKeysForEnvironment(environment) {
1161
+ const normalized = normalizeEnvironment(environment || '');
1162
+ if (normalized === 'prod') return ['prodPipeline', 'pipeline'];
1163
+ if (normalized === 'gray') return ['grayPipeline', 'prodPipeline', 'pipeline'];
1164
+ return ['pipeline', 'grayPipeline', 'prodPipeline'];
1165
+ }
1166
+
1167
+ function parsePipelineDescriptor(value) {
1168
+ if (!value) return null;
1169
+ const parts = String(value).split('#@').map((item) => item.trim()).filter(Boolean);
1170
+ if (parts.length < 2) return null;
1171
+ return {
1172
+ pipelineName: parts[0] || null,
1173
+ pipelineId: parts[1] || null
1174
+ };
1175
+ }
1176
+
1177
+ function getEnvLookupOrder(environment) {
1178
+ const normalized = normalizeEnvironment(environment || '');
1179
+ if (normalized === 'prod') return ['prod'];
1180
+ if (normalized === 'gray') return ['gray', 'prod'];
1181
+ if (normalized === 'uat') return ['uat'];
1182
+ if (normalized === 'test') return ['test', 'dev', 'uat'];
1183
+ return ['uat', 'test', 'dev', 'gray', 'prod'];
1184
+ }
1185
+
1186
+ function buildPrefixedPipelineName(serviceName, environment) {
1187
+ if (!serviceName) return null;
1188
+ const normalized = normalizeEnvironment(environment || '');
1189
+ if (!normalized) return null;
1190
+ const prefix = normalized === 'test' ? 'test' : normalized;
1191
+ return `${prefix}-${serviceName}`;
1192
+ }
1193
+
1194
+ function inferEnvironmentFromPipelineName(pipelineName) {
1195
+ const match = String(pipelineName || '').match(/^(test|dev|uat|gray|prod)-/i);
1196
+ return match ? (ENV_PREFIX_TO_ENV[match[1].toLowerCase()] || normalizeEnvironment(match[1])) : null;
1197
+ }
1198
+
1199
+ function collectNestedValues(payload, values = []) {
1200
+ if (payload == null) {
1201
+ return values;
1202
+ }
1203
+
1204
+ if (Array.isArray(payload)) {
1205
+ for (const item of payload) {
1206
+ collectNestedValues(item, values);
1207
+ }
1208
+ return values;
1209
+ }
1210
+
1211
+ if (typeof payload === 'object') {
1212
+ for (const value of Object.values(payload)) {
1213
+ collectNestedValues(value, values);
1214
+ }
1215
+ return values;
1216
+ }
1217
+
1218
+ values.push(payload);
1219
+ return values;
1220
+ }
1221
+
397
1222
  function compactObject(value) {
398
1223
  return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== null && item !== ''));
399
1224
  }
1225
+
1226
+ function getWorkflowMcpServer(workflow) {
1227
+ return workflow.client?.mcpServer || workflow.binding?.mcpServer || workflow.provider;
1228
+ }