@synergenius/flow-weaver 0.8.3 → 0.9.1
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/dist/annotation-generator.d.ts +6 -1
- package/dist/annotation-generator.js +75 -24
- package/dist/api/generate.d.ts +5 -0
- package/dist/api/generate.js +8 -1
- package/dist/api/validate.d.ts +5 -2
- package/dist/api/validate.js +4 -4
- package/dist/ast/types.d.ts +2 -0
- package/dist/cli/commands/implement.d.ts +9 -0
- package/dist/cli/commands/implement.js +96 -0
- package/dist/cli/commands/status.d.ts +9 -0
- package/dist/cli/commands/status.js +121 -0
- package/dist/cli/flow-weaver.mjs +1067 -499
- package/dist/cli/index.js +32 -0
- package/dist/diagram/html-viewer.js +54 -18
- package/dist/diagram/renderer.js +5 -4
- package/dist/generator/unified.js +5 -1
- package/dist/mcp/server.js +2 -0
- package/dist/mcp/tools-model.d.ts +3 -0
- package/dist/mcp/tools-model.js +253 -0
- package/dist/utils/port-ordering.d.ts +5 -5
- package/dist/utils/port-ordering.js +34 -41
- package/dist/validator.d.ts +2 -0
- package/dist/validator.js +41 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -31,6 +31,8 @@ import { migrateCommand } from './commands/migrate.js';
|
|
|
31
31
|
import { changelogCommand } from './commands/changelog.js';
|
|
32
32
|
import { stripCommand } from './commands/strip.js';
|
|
33
33
|
import { docsListCommand, docsReadCommand, docsSearchCommand } from './commands/docs.js';
|
|
34
|
+
import { statusCommand } from './commands/status.js';
|
|
35
|
+
import { implementCommand } from './commands/implement.js';
|
|
34
36
|
import { marketInitCommand, marketPackCommand, marketPublishCommand, marketInstallCommand, marketSearchCommand, marketListCommand, } from './commands/market.js';
|
|
35
37
|
import { logger } from './utils/logger.js';
|
|
36
38
|
import { getErrorMessage } from '../utils/error-utils.js';
|
|
@@ -589,6 +591,36 @@ program
|
|
|
589
591
|
process.exit(1);
|
|
590
592
|
}
|
|
591
593
|
});
|
|
594
|
+
// Status command
|
|
595
|
+
program
|
|
596
|
+
.command('status <input>')
|
|
597
|
+
.description('Report implementation progress for stub workflows')
|
|
598
|
+
.option('-w, --workflow-name <name>', 'Specific workflow name')
|
|
599
|
+
.option('--json', 'Output as JSON', false)
|
|
600
|
+
.action(async (input, options) => {
|
|
601
|
+
try {
|
|
602
|
+
await statusCommand(input, options);
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
logger.error(`Command failed: ${getErrorMessage(error)}`);
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
// Implement command
|
|
610
|
+
program
|
|
611
|
+
.command('implement <input> <node>')
|
|
612
|
+
.description('Replace a stub node with a real function skeleton')
|
|
613
|
+
.option('-w, --workflow-name <name>', 'Specific workflow name')
|
|
614
|
+
.option('-p, --preview', 'Preview the generated code without writing', false)
|
|
615
|
+
.action(async (input, node, options) => {
|
|
616
|
+
try {
|
|
617
|
+
await implementCommand(input, node, options);
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
logger.error(`Command failed: ${getErrorMessage(error)}`);
|
|
621
|
+
process.exit(1);
|
|
622
|
+
}
|
|
623
|
+
});
|
|
592
624
|
// Changelog command
|
|
593
625
|
program
|
|
594
626
|
.command('changelog')
|
|
@@ -266,43 +266,79 @@ body.node-active .connections path.dimmed { opacity: 0.15; }
|
|
|
266
266
|
else if (e.key === 'Escape') deselectNode();
|
|
267
267
|
});
|
|
268
268
|
|
|
269
|
-
// ---- Port label visibility
|
|
270
|
-
var
|
|
271
|
-
|
|
269
|
+
// ---- Port label visibility ----
|
|
270
|
+
var labelMap = {};
|
|
271
|
+
content.querySelectorAll('.labels g[data-port-label]').forEach(function(lbl) {
|
|
272
|
+
labelMap[lbl.getAttribute('data-port-label')] = lbl;
|
|
273
|
+
});
|
|
272
274
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
275
|
+
// Build adjacency: portId → array of connected portIds
|
|
276
|
+
var portConnections = {};
|
|
277
|
+
content.querySelectorAll('.connections path').forEach(function(p) {
|
|
278
|
+
var src = p.getAttribute('data-source');
|
|
279
|
+
var tgt = p.getAttribute('data-target');
|
|
280
|
+
if (!src || !tgt) return;
|
|
281
|
+
if (!portConnections[src]) portConnections[src] = [];
|
|
282
|
+
if (!portConnections[tgt]) portConnections[tgt] = [];
|
|
283
|
+
portConnections[src].push(tgt);
|
|
284
|
+
portConnections[tgt].push(src);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
var allLabelIds = Object.keys(labelMap);
|
|
288
|
+
var hoveredPort = null;
|
|
289
|
+
|
|
290
|
+
function showLabel(id) { var l = labelMap[id]; if (l) { l.style.opacity = '1'; l.style.pointerEvents = 'auto'; } }
|
|
291
|
+
function hideLabel(id) { var l = labelMap[id]; if (l) { l.style.opacity = '0'; l.style.pointerEvents = 'none'; } }
|
|
292
|
+
|
|
293
|
+
function showLabelsFor(nodeId) {
|
|
294
|
+
allLabelIds.forEach(function(id) {
|
|
295
|
+
if (id.indexOf(nodeId + '.') === 0) showLabel(id);
|
|
280
296
|
});
|
|
281
297
|
}
|
|
282
|
-
function hideLabelsFor(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (portId.indexOf(id + '.') === 0) {
|
|
286
|
-
lbl.style.opacity = '0';
|
|
287
|
-
lbl.style.pointerEvents = 'none';
|
|
288
|
-
}
|
|
298
|
+
function hideLabelsFor(nodeId) {
|
|
299
|
+
allLabelIds.forEach(function(id) {
|
|
300
|
+
if (id.indexOf(nodeId + '.') === 0) hideLabel(id);
|
|
289
301
|
});
|
|
290
302
|
}
|
|
291
303
|
|
|
304
|
+
// Node hover: show all port labels for the hovered node
|
|
305
|
+
var nodeEls = content.querySelectorAll('.nodes g[data-node-id]');
|
|
292
306
|
nodeEls.forEach(function(nodeG) {
|
|
293
307
|
var nodeId = nodeG.getAttribute('data-node-id');
|
|
294
308
|
var parentNodeG = nodeG.parentElement ? nodeG.parentElement.closest('g[data-node-id]') : null;
|
|
295
309
|
var parentId = parentNodeG ? parentNodeG.getAttribute('data-node-id') : null;
|
|
296
310
|
nodeG.addEventListener('mouseenter', function() {
|
|
311
|
+
if (hoveredPort) return; // port hover takes priority
|
|
297
312
|
if (parentId) hideLabelsFor(parentId);
|
|
298
313
|
showLabelsFor(nodeId);
|
|
299
314
|
});
|
|
300
315
|
nodeG.addEventListener('mouseleave', function() {
|
|
316
|
+
if (hoveredPort) return;
|
|
301
317
|
hideLabelsFor(nodeId);
|
|
302
318
|
if (parentId) showLabelsFor(parentId);
|
|
303
319
|
});
|
|
304
320
|
});
|
|
305
321
|
|
|
322
|
+
// Port hover: show this port's label + all connected port labels
|
|
323
|
+
content.querySelectorAll('[data-port-id]').forEach(function(portEl) {
|
|
324
|
+
var portId = portEl.getAttribute('data-port-id');
|
|
325
|
+
var nodeId = portId.split('.')[0];
|
|
326
|
+
var peers = (portConnections[portId] || []).concat(portId);
|
|
327
|
+
|
|
328
|
+
portEl.addEventListener('mouseenter', function() {
|
|
329
|
+
hoveredPort = portId;
|
|
330
|
+
// Hide all labels for this node first, then show only the relevant ones
|
|
331
|
+
hideLabelsFor(nodeId);
|
|
332
|
+
peers.forEach(showLabel);
|
|
333
|
+
});
|
|
334
|
+
portEl.addEventListener('mouseleave', function() {
|
|
335
|
+
hoveredPort = null;
|
|
336
|
+
peers.forEach(hideLabel);
|
|
337
|
+
// Restore all labels for the node since we're still inside it
|
|
338
|
+
showLabelsFor(nodeId);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
306
342
|
// ---- Click to inspect node ----
|
|
307
343
|
function deselectNode() {
|
|
308
344
|
selectedNodeId = null;
|
|
@@ -329,7 +365,7 @@ body.node-active .connections path.dimmed { opacity: 0.15; }
|
|
|
329
365
|
ports.forEach(function(p) {
|
|
330
366
|
var id = p.getAttribute('data-port-id');
|
|
331
367
|
var dir = p.getAttribute('data-direction');
|
|
332
|
-
var name = id.split('.').slice(1).join('.');
|
|
368
|
+
var name = id.split('.').slice(1).join('.').replace(/:(?:input|output)$/, '');
|
|
333
369
|
if (dir === 'input') inputs.push(name);
|
|
334
370
|
else outputs.push(name);
|
|
335
371
|
});
|
package/dist/diagram/renderer.js
CHANGED
|
@@ -90,14 +90,14 @@ export function renderSVG(graph, options = {}) {
|
|
|
90
90
|
// ---- Connection rendering ----
|
|
91
91
|
function renderConnection(parts, conn, gradIndex) {
|
|
92
92
|
const dashAttr = conn.isStepConnection ? '' : ' stroke-dasharray="8 4"';
|
|
93
|
-
parts.push(` <path d="${conn.path}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="3"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}"/>`);
|
|
93
|
+
parts.push(` <path d="${conn.path}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="3"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}:output" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}:input"/>`);
|
|
94
94
|
}
|
|
95
95
|
function renderScopeConnection(parts, conn, allConnections) {
|
|
96
96
|
const gradIndex = allConnections.indexOf(conn);
|
|
97
97
|
if (gradIndex < 0)
|
|
98
98
|
return;
|
|
99
99
|
const dashAttr = conn.isStepConnection ? '' : ' stroke-dasharray="8 4"';
|
|
100
|
-
parts.push(` <path d="${conn.path}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="2.5"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}"/>`);
|
|
100
|
+
parts.push(` <path d="${conn.path}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="2.5"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}:output" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}:input"/>`);
|
|
101
101
|
}
|
|
102
102
|
// ---- Node rendering ----
|
|
103
103
|
/** Render node body rect + icon */
|
|
@@ -183,7 +183,7 @@ function renderPortDots(parts, nodeId, inputs, outputs, themeName) {
|
|
|
183
183
|
const color = getPortColor(port.dataType, port.isFailure, themeName);
|
|
184
184
|
const ringColor = getPortRingColor(port.dataType, port.isFailure, themeName);
|
|
185
185
|
const dir = port.direction === 'INPUT' ? 'input' : 'output';
|
|
186
|
-
parts.push(` <circle cx="${port.cx}" cy="${port.cy}" r="${PORT_RADIUS}" fill="${color}" stroke="${ringColor}" stroke-width="2" data-port-id="${escapeXml(nodeId)}.${escapeXml(port.name)}" data-direction="${dir}"/>`);
|
|
186
|
+
parts.push(` <circle cx="${port.cx}" cy="${port.cy}" r="${PORT_RADIUS}" fill="${color}" stroke="${ringColor}" stroke-width="2" data-port-id="${escapeXml(nodeId)}.${escapeXml(port.name)}:${dir}" data-direction="${dir}"/>`);
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
/** Render only port label badges (no dots) */
|
|
@@ -192,7 +192,8 @@ function renderPortLabels(parts, nodeId, inputs, outputs, theme, themeName) {
|
|
|
192
192
|
const color = getPortColor(port.dataType, port.isFailure, themeName);
|
|
193
193
|
const isInput = port.direction === 'INPUT';
|
|
194
194
|
const abbrev = TYPE_ABBREVIATIONS[port.dataType] ?? port.dataType;
|
|
195
|
-
const
|
|
195
|
+
const dir = isInput ? 'input' : 'output';
|
|
196
|
+
const portId = `${escapeXml(nodeId)}.${escapeXml(port.name)}:${dir}`;
|
|
196
197
|
const portLabel = port.label;
|
|
197
198
|
const typeWidth = measureText(abbrev);
|
|
198
199
|
const labelWidth = measureText(portLabel);
|
|
@@ -1487,7 +1487,11 @@ branchingNodes = new Set() // Branching nodes set for port-aware STEP guards
|
|
|
1487
1487
|
});
|
|
1488
1488
|
const resultVar = `${safeId}Result`;
|
|
1489
1489
|
const awaitKeyword = nodeType.isAsync ? 'await ' : '';
|
|
1490
|
-
if (nodeType.variant === '
|
|
1490
|
+
if (nodeType.variant === 'STUB') {
|
|
1491
|
+
// Stub node: emit a runtime throw. The workflow was generated with generateStubs: true.
|
|
1492
|
+
lines.push(`${indent} throw new Error('Node "${instanceId}" uses stub type "${functionName}" which has no implementation.');`);
|
|
1493
|
+
}
|
|
1494
|
+
else if (nodeType.variant === 'COERCION') {
|
|
1491
1495
|
// Coercion node: inline JS expression instead of function call
|
|
1492
1496
|
const coerceExprMap = {
|
|
1493
1497
|
__fw_toString: 'String',
|
package/dist/mcp/server.js
CHANGED
|
@@ -12,6 +12,7 @@ import { registerExportTools } from './tools-export.js';
|
|
|
12
12
|
import { registerMarketplaceTools } from './tools-marketplace.js';
|
|
13
13
|
import { registerDiagramTools } from './tools-diagram.js';
|
|
14
14
|
import { registerDocsTools } from './tools-docs.js';
|
|
15
|
+
import { registerModelTools } from './tools-model.js';
|
|
15
16
|
import { registerResources } from './resources.js';
|
|
16
17
|
function parseEventFilterFromEnv() {
|
|
17
18
|
const filter = {};
|
|
@@ -72,6 +73,7 @@ export async function startMcpServer(options) {
|
|
|
72
73
|
registerMarketplaceTools(mcp);
|
|
73
74
|
registerDiagramTools(mcp);
|
|
74
75
|
registerDocsTools(mcp);
|
|
76
|
+
registerModelTools(mcp);
|
|
75
77
|
registerResources(mcp, connection, buffer);
|
|
76
78
|
// Connect transport (only in stdio MCP mode)
|
|
77
79
|
if (!options._testDeps && options.stdio) {
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { parseWorkflow, validateWorkflow } from '../api/index.js';
|
|
5
|
+
import { generateFunctionSignature } from '../annotation-generator.js';
|
|
6
|
+
import { makeToolResult, makeErrorResult } from './response-utils.js';
|
|
7
|
+
const stepSchema = z.object({
|
|
8
|
+
name: z.string().describe('Function name for this node'),
|
|
9
|
+
description: z.string().optional().describe('Brief description of what this node does'),
|
|
10
|
+
inputs: z.record(z.string()).describe('Input ports as { portName: dataType } (e.g. { email: "STRING" })'),
|
|
11
|
+
outputs: z.record(z.string()).describe('Output ports as { portName: dataType }'),
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* Find a `declare function <name>(...): ...;` declaration in source text,
|
|
15
|
+
* handling multiline signatures. Returns the full matched text and its
|
|
16
|
+
* leading indentation, or null if not found.
|
|
17
|
+
*/
|
|
18
|
+
function findDeclareFunction(source, functionName) {
|
|
19
|
+
const lines = source.split('\n');
|
|
20
|
+
const startPattern = new RegExp(`^(\\s*)declare\\s+function\\s+${escapeRegex(functionName)}\\s*\\(`);
|
|
21
|
+
for (let i = 0; i < lines.length; i++) {
|
|
22
|
+
const m = lines[i].match(startPattern);
|
|
23
|
+
if (!m)
|
|
24
|
+
continue;
|
|
25
|
+
const indent = m[1] || '';
|
|
26
|
+
// Accumulate lines until we find one ending with ;
|
|
27
|
+
let accumulated = lines[i];
|
|
28
|
+
let j = i;
|
|
29
|
+
while (!accumulated.trimEnd().endsWith(';') && j < lines.length - 1) {
|
|
30
|
+
j++;
|
|
31
|
+
accumulated += '\n' + lines[j];
|
|
32
|
+
}
|
|
33
|
+
return { match: accumulated, indent };
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
export function registerModelTools(mcp) {
|
|
38
|
+
mcp.tool('fw_create_model', 'Create a new stub workflow model from a structured description. ' +
|
|
39
|
+
'Generates a TypeScript file with declare function stubs and workflow annotations. ' +
|
|
40
|
+
'The model can be validated structurally before any node is implemented.', {
|
|
41
|
+
name: z.string().describe('Workflow name (used as export name)'),
|
|
42
|
+
description: z.string().optional().describe('Workflow description'),
|
|
43
|
+
steps: z.array(stepSchema).min(1).describe('Node definitions in execution order'),
|
|
44
|
+
flow: z
|
|
45
|
+
.string()
|
|
46
|
+
.describe('Execution path, e.g. "Start -> validateEmail -> checkUserExists -> Exit"'),
|
|
47
|
+
filePath: z.string().describe('Output file path'),
|
|
48
|
+
}, async (args) => {
|
|
49
|
+
try {
|
|
50
|
+
const outputPath = path.resolve(args.filePath);
|
|
51
|
+
const lines = [];
|
|
52
|
+
// Generate declare function stubs
|
|
53
|
+
for (const step of args.steps) {
|
|
54
|
+
lines.push('/** @flowWeaver node */');
|
|
55
|
+
const params = Object.entries(step.inputs)
|
|
56
|
+
.map(([name, type]) => `${name}: ${mapTypeToTS(type)}`)
|
|
57
|
+
.join(', ');
|
|
58
|
+
const outputEntries = Object.entries(step.outputs);
|
|
59
|
+
const returnType = outputEntries.length === 1
|
|
60
|
+
? mapTypeToTS(outputEntries[0][1])
|
|
61
|
+
: `{ ${outputEntries.map(([name, type]) => `${name}: ${mapTypeToTS(type)}`).join('; ')} }`;
|
|
62
|
+
lines.push(`declare function ${step.name}(${params}): ${returnType};`);
|
|
63
|
+
lines.push('');
|
|
64
|
+
}
|
|
65
|
+
// Parse the flow string — use full function names as instance IDs
|
|
66
|
+
const flowSteps = args.flow
|
|
67
|
+
.split('->')
|
|
68
|
+
.map((s) => s.trim())
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
// Build @node annotations using full name as both ID and type
|
|
71
|
+
const nodeAnnotations = args.steps.map((step) => `@node ${step.name} ${step.name}`);
|
|
72
|
+
// @path uses the same full names directly from the flow string
|
|
73
|
+
const pathAnnotation = `@path ${flowSteps.join(' -> ')}`;
|
|
74
|
+
// Generate workflow annotation
|
|
75
|
+
const jsdocLines = ['/**'];
|
|
76
|
+
if (args.description) {
|
|
77
|
+
jsdocLines.push(` * ${args.description}`);
|
|
78
|
+
jsdocLines.push(' *');
|
|
79
|
+
}
|
|
80
|
+
jsdocLines.push(' * @flowWeaver workflow @autoConnect');
|
|
81
|
+
for (const nodeAnn of nodeAnnotations) {
|
|
82
|
+
jsdocLines.push(` * ${nodeAnn}`);
|
|
83
|
+
}
|
|
84
|
+
jsdocLines.push(` * ${pathAnnotation}`);
|
|
85
|
+
jsdocLines.push(' */');
|
|
86
|
+
jsdocLines.push(`export const ${args.name} = 'flowWeaver:draft';`);
|
|
87
|
+
lines.push(...jsdocLines);
|
|
88
|
+
lines.push('');
|
|
89
|
+
const content = lines.join('\n');
|
|
90
|
+
const dir = path.dirname(outputPath);
|
|
91
|
+
if (!fs.existsSync(dir)) {
|
|
92
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
fs.writeFileSync(outputPath, content, 'utf8');
|
|
95
|
+
return makeToolResult({
|
|
96
|
+
filePath: outputPath,
|
|
97
|
+
workflowName: args.name,
|
|
98
|
+
stubCount: args.steps.length,
|
|
99
|
+
nodes: args.steps.map((s) => s.name),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
return makeErrorResult('CREATE_MODEL_ERROR', `fw_create_model failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
mcp.tool('fw_workflow_status', 'Report implementation progress for a workflow — which nodes are stubs, ' +
|
|
107
|
+
'which are implemented, and whether the graph structure is valid.', {
|
|
108
|
+
filePath: z.string().describe('Path to the workflow file'),
|
|
109
|
+
workflowName: z.string().optional().describe('Specific workflow name if file has multiple'),
|
|
110
|
+
}, async (args) => {
|
|
111
|
+
try {
|
|
112
|
+
const filePath = path.resolve(args.filePath);
|
|
113
|
+
const parseResult = await parseWorkflow(filePath, { workflowName: args.workflowName });
|
|
114
|
+
if (parseResult.errors.length > 0) {
|
|
115
|
+
return makeErrorResult('PARSE_ERROR', `Parse errors:\n${parseResult.errors.join('\n')}`);
|
|
116
|
+
}
|
|
117
|
+
const ast = parseResult.ast;
|
|
118
|
+
const instanceTypeMap = new Map();
|
|
119
|
+
for (const nt of ast.nodeTypes) {
|
|
120
|
+
instanceTypeMap.set(nt.name, nt);
|
|
121
|
+
if (nt.functionName !== nt.name) {
|
|
122
|
+
instanceTypeMap.set(nt.functionName, nt);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const nodes = [];
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
for (const instance of ast.instances) {
|
|
128
|
+
const nt = instanceTypeMap.get(instance.nodeType);
|
|
129
|
+
if (!nt || seen.has(nt.functionName))
|
|
130
|
+
continue;
|
|
131
|
+
seen.add(nt.functionName);
|
|
132
|
+
const inputs = {};
|
|
133
|
+
const outputs = {};
|
|
134
|
+
for (const [name, port] of Object.entries(nt.inputs)) {
|
|
135
|
+
if (name === 'execute')
|
|
136
|
+
continue;
|
|
137
|
+
inputs[name] = port.dataType;
|
|
138
|
+
}
|
|
139
|
+
for (const [name, port] of Object.entries(nt.outputs)) {
|
|
140
|
+
if (name === 'onSuccess' || name === 'onFailure')
|
|
141
|
+
continue;
|
|
142
|
+
outputs[name] = port.dataType;
|
|
143
|
+
}
|
|
144
|
+
nodes.push({
|
|
145
|
+
name: nt.functionName,
|
|
146
|
+
status: nt.variant === 'STUB' ? 'stub' : 'implemented',
|
|
147
|
+
inputs,
|
|
148
|
+
outputs,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const implemented = nodes.filter((n) => n.status === 'implemented').length;
|
|
152
|
+
const total = nodes.length;
|
|
153
|
+
// Draft validation for structural issues
|
|
154
|
+
const validation = validateWorkflow(ast, { mode: 'draft' });
|
|
155
|
+
const structuralErrors = validation.errors
|
|
156
|
+
.filter((e) => e.code !== 'STUB_NODE')
|
|
157
|
+
.map((e) => ({ message: e.message, code: e.code, node: e.node }));
|
|
158
|
+
return makeToolResult({
|
|
159
|
+
workflowName: ast.name,
|
|
160
|
+
implemented,
|
|
161
|
+
total,
|
|
162
|
+
percentage: total > 0 ? Math.round((implemented / total) * 100) : 100,
|
|
163
|
+
nodes,
|
|
164
|
+
structurallyValid: structuralErrors.length === 0,
|
|
165
|
+
structuralErrors,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
return makeErrorResult('STATUS_ERROR', `fw_workflow_status failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
mcp.tool('fw_implement_node', 'Replace a stub node (declare function) with a real function skeleton, ' +
|
|
173
|
+
'or write a provided implementation directly. Preserves existing JSDoc annotations.', {
|
|
174
|
+
filePath: z.string().describe('Path to the workflow file'),
|
|
175
|
+
nodeName: z.string().describe('Function name of the stub node to implement'),
|
|
176
|
+
implementation: z
|
|
177
|
+
.string()
|
|
178
|
+
.optional()
|
|
179
|
+
.describe('Full function body to write. If omitted, generates a skeleton.'),
|
|
180
|
+
workflowName: z.string().optional().describe('Specific workflow name'),
|
|
181
|
+
}, async (args) => {
|
|
182
|
+
try {
|
|
183
|
+
const filePath = path.resolve(args.filePath);
|
|
184
|
+
if (!fs.existsSync(filePath)) {
|
|
185
|
+
return makeErrorResult('FILE_NOT_FOUND', `File not found: ${args.filePath}`);
|
|
186
|
+
}
|
|
187
|
+
const parseResult = await parseWorkflow(filePath, { workflowName: args.workflowName });
|
|
188
|
+
if (parseResult.errors.length > 0) {
|
|
189
|
+
return makeErrorResult('PARSE_ERROR', `Parse errors:\n${parseResult.errors.join('\n')}`);
|
|
190
|
+
}
|
|
191
|
+
const ast = parseResult.ast;
|
|
192
|
+
const stubNt = ast.nodeTypes.find((nt) => nt.variant === 'STUB' && (nt.functionName === args.nodeName || nt.name === args.nodeName));
|
|
193
|
+
if (!stubNt) {
|
|
194
|
+
const existing = ast.nodeTypes.find((nt) => nt.functionName === args.nodeName || nt.name === args.nodeName);
|
|
195
|
+
if (existing) {
|
|
196
|
+
return makeErrorResult('ALREADY_IMPLEMENTED', `Node "${args.nodeName}" is already implemented.`);
|
|
197
|
+
}
|
|
198
|
+
const stubs = ast.nodeTypes
|
|
199
|
+
.filter((nt) => nt.variant === 'STUB')
|
|
200
|
+
.map((nt) => nt.functionName);
|
|
201
|
+
return makeErrorResult('NODE_NOT_FOUND', `Stub "${args.nodeName}" not found. Available stubs: ${stubs.join(', ') || 'none'}`);
|
|
202
|
+
}
|
|
203
|
+
const source = fs.readFileSync(filePath, 'utf8');
|
|
204
|
+
const found = findDeclareFunction(source, stubNt.functionName);
|
|
205
|
+
if (!found) {
|
|
206
|
+
return makeErrorResult('SOURCE_MISMATCH', `Could not find "declare function ${stubNt.functionName}" in source file.`);
|
|
207
|
+
}
|
|
208
|
+
let replacement;
|
|
209
|
+
if (args.implementation) {
|
|
210
|
+
replacement = args.implementation
|
|
211
|
+
.split('\n')
|
|
212
|
+
.map((line) => found.indent + line)
|
|
213
|
+
.join('\n');
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
const implementedType = { ...stubNt, variant: 'FUNCTION' };
|
|
217
|
+
const signatureLines = generateFunctionSignature(implementedType);
|
|
218
|
+
replacement = signatureLines.map((line) => found.indent + line).join('\n');
|
|
219
|
+
}
|
|
220
|
+
const updated = source.replace(found.match, replacement);
|
|
221
|
+
fs.writeFileSync(filePath, updated, 'utf8');
|
|
222
|
+
return makeToolResult({
|
|
223
|
+
nodeName: stubNt.functionName,
|
|
224
|
+
filePath,
|
|
225
|
+
action: args.implementation ? 'implemented' : 'scaffolded',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
return makeErrorResult('IMPLEMENT_ERROR', `fw_implement_node failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
function mapTypeToTS(dataType) {
|
|
234
|
+
const upper = dataType.toUpperCase();
|
|
235
|
+
switch (upper) {
|
|
236
|
+
case 'STRING':
|
|
237
|
+
return 'string';
|
|
238
|
+
case 'NUMBER':
|
|
239
|
+
return 'number';
|
|
240
|
+
case 'BOOLEAN':
|
|
241
|
+
return 'boolean';
|
|
242
|
+
case 'OBJECT':
|
|
243
|
+
return 'Record<string, unknown>';
|
|
244
|
+
case 'ARRAY':
|
|
245
|
+
return 'unknown[]';
|
|
246
|
+
default:
|
|
247
|
+
return 'unknown';
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function escapeRegex(str) {
|
|
251
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
252
|
+
}
|
|
253
|
+
//# sourceMappingURL=tools-model.js.map
|
|
@@ -14,11 +14,11 @@ export declare function isMandatoryPort(portName: string, isScoped: boolean): bo
|
|
|
14
14
|
*
|
|
15
15
|
* Rules:
|
|
16
16
|
* 1. Ports are grouped by scope (undefined = external, string = scoped)
|
|
17
|
-
* 2.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* 2. Explicit order metadata is always preserved
|
|
18
|
+
* 3. Mandatory ports without explicit orders get negative slots (-N, ..., -1)
|
|
19
|
+
* so they always sort before any user-specified [order:0] data port
|
|
20
|
+
* 4. Regular ports without explicit orders fill non-negative slots (0+),
|
|
21
|
+
* skipping any slots already occupied by explicit orders
|
|
22
22
|
*
|
|
23
23
|
* @param ports - Record of port definitions to process (mutated in place)
|
|
24
24
|
*/
|
|
@@ -19,11 +19,11 @@ export function isMandatoryPort(portName, isScoped) {
|
|
|
19
19
|
*
|
|
20
20
|
* Rules:
|
|
21
21
|
* 1. Ports are grouped by scope (undefined = external, string = scoped)
|
|
22
|
-
* 2.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* 2. Explicit order metadata is always preserved
|
|
23
|
+
* 3. Mandatory ports without explicit orders get negative slots (-N, ..., -1)
|
|
24
|
+
* so they always sort before any user-specified [order:0] data port
|
|
25
|
+
* 4. Regular ports without explicit orders fill non-negative slots (0+),
|
|
26
|
+
* skipping any slots already occupied by explicit orders
|
|
27
27
|
*
|
|
28
28
|
* @param ports - Record of port definitions to process (mutated in place)
|
|
29
29
|
*/
|
|
@@ -40,48 +40,41 @@ export function assignImplicitPortOrders(ports) {
|
|
|
40
40
|
// Process each scope group independently
|
|
41
41
|
for (const [scope, portsInScope] of scopeGroups.entries()) {
|
|
42
42
|
const isScoped = scope !== undefined;
|
|
43
|
-
//
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
// Find minimum explicit order among regular ports (if any)
|
|
47
|
-
let minRegularExplicitOrder = Infinity;
|
|
48
|
-
for (const [, portDef] of regularPorts) {
|
|
43
|
+
// Collect all explicitly occupied order slots
|
|
44
|
+
const occupied = new Set();
|
|
45
|
+
for (const [, portDef] of portsInScope) {
|
|
49
46
|
const order = portDef.metadata?.order;
|
|
50
47
|
if (typeof order === "number") {
|
|
51
|
-
|
|
48
|
+
occupied.add(order);
|
|
52
49
|
}
|
|
53
50
|
}
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const regularPortsWithOrder0 = regularPorts.filter(([, p]) => p.metadata?.order === 0);
|
|
61
|
-
mandatoryStartOrder = regularPortsWithOrder0.length;
|
|
51
|
+
// Helper: find next available slot starting from `from`
|
|
52
|
+
function nextSlot(from) {
|
|
53
|
+
while (occupied.has(from))
|
|
54
|
+
from++;
|
|
55
|
+
occupied.add(from);
|
|
56
|
+
return from;
|
|
62
57
|
}
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
58
|
+
// Separate mandatory from regular ports (only those needing implicit orders)
|
|
59
|
+
const mandatoryNeedOrder = portsInScope.filter(([name, def]) => isMandatoryPort(name, isScoped) && def.metadata?.order === undefined);
|
|
60
|
+
const regularNeedOrder = portsInScope.filter(([name, def]) => !isMandatoryPort(name, isScoped) && def.metadata?.order === undefined);
|
|
61
|
+
// Mandatory ports fill negative slots so they always sort before [order:0] data ports
|
|
62
|
+
let slot = -mandatoryNeedOrder.length;
|
|
63
|
+
for (const [, portDef] of mandatoryNeedOrder) {
|
|
64
|
+
if (!portDef.metadata)
|
|
65
|
+
portDef.metadata = {};
|
|
66
|
+
slot = nextSlot(slot);
|
|
67
|
+
portDef.metadata.order = slot;
|
|
68
|
+
slot++;
|
|
73
69
|
}
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
portDef.metadata.order = currentRegularOrder++;
|
|
84
|
-
}
|
|
70
|
+
// Regular ports fill non-negative slots, skipping occupied ones
|
|
71
|
+
slot = Math.max(slot, 0);
|
|
72
|
+
for (const [, portDef] of regularNeedOrder) {
|
|
73
|
+
if (!portDef.metadata)
|
|
74
|
+
portDef.metadata = {};
|
|
75
|
+
slot = nextSlot(slot);
|
|
76
|
+
portDef.metadata.order = slot;
|
|
77
|
+
slot++;
|
|
85
78
|
}
|
|
86
79
|
}
|
|
87
80
|
}
|
package/dist/validator.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export declare class WorkflowValidator {
|
|
|
4
4
|
private errors;
|
|
5
5
|
private warnings;
|
|
6
6
|
private strictMode;
|
|
7
|
+
private draftMode;
|
|
7
8
|
/** Look up instance sourceLocation by instance ID */
|
|
8
9
|
private getInstanceLocation;
|
|
9
10
|
/** Look up connection sourceLocation */
|
|
@@ -26,6 +27,7 @@ export declare class WorkflowValidator {
|
|
|
26
27
|
validateNodeType(nodeType: TNodeTypeAST): string[];
|
|
27
28
|
validate(workflow: TWorkflowAST, options?: {
|
|
28
29
|
strictMode?: boolean;
|
|
30
|
+
mode?: 'strict' | 'draft';
|
|
29
31
|
}): {
|
|
30
32
|
valid: boolean;
|
|
31
33
|
errors: TValidationError[];
|