@synergenius/flow-weaver 0.20.1 → 0.20.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.
- package/dist/annotation-generator.js +7 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/modify-operation.d.ts +15 -0
- package/dist/api/modify-operation.js +177 -0
- package/dist/api/validate.js +17 -0
- package/dist/ast/types.d.ts +2 -0
- package/dist/chevrotain-parser/node-parser.d.ts +2 -0
- package/dist/chevrotain-parser/node-parser.js +27 -1
- package/dist/chevrotain-parser/tokens.d.ts +1 -0
- package/dist/chevrotain-parser/tokens.js +5 -0
- package/dist/cli/commands/modify.d.ts +29 -0
- package/dist/cli/commands/modify.js +57 -0
- package/dist/cli/flow-weaver.mjs +44885 -44640
- package/dist/cli/index.js +73 -2
- package/dist/cli/templates/workflows/aggregator.js +3 -3
- package/dist/cli/templates/workflows/ai-agent.js +3 -3
- package/dist/cli/templates/workflows/ai-chat.js +5 -4
- package/dist/cli/templates/workflows/ai-rag.js +3 -3
- package/dist/cli/templates/workflows/ai-react.js +12 -12
- package/dist/cli/templates/workflows/conditional.js +3 -3
- package/dist/cli/templates/workflows/error-handler.js +2 -2
- package/dist/cli/templates/workflows/foreach.js +3 -3
- package/dist/cli/templates/workflows/sequential.js +7 -4
- package/dist/cli/templates/workflows/webhook.js +3 -3
- package/dist/generated-version.d.ts +1 -1
- package/dist/generated-version.js +1 -1
- package/dist/jsdoc-parser.d.ts +1 -0
- package/dist/jsdoc-parser.js +4 -3
- package/dist/mcp/tools-pattern.js +1 -180
- package/dist/parser.js +1 -0
- package/dist/validator.js +15 -0
- package/docs/reference/advanced-annotations.md +11 -0
- package/docs/reference/cli-reference.md +118 -1
- package/docs/reference/error-codes.md +1 -1
- package/docs/reference/jsdoc-grammar.md +4 -2
- package/docs/reference/marketplace.md +74 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -39,6 +39,7 @@ import { docsListCommand, docsReadCommand, docsSearchCommand } from './commands/
|
|
|
39
39
|
import { contextCommand } from './commands/context.js';
|
|
40
40
|
import { statusCommand } from './commands/status.js';
|
|
41
41
|
import { implementCommand } from './commands/implement.js';
|
|
42
|
+
import { modifyAddNodeCommand, modifyRemoveNodeCommand, modifyAddConnectionCommand, modifyRemoveConnectionCommand, modifyRenameNodeCommand, modifySetPositionCommand, modifySetLabelCommand, } from './commands/modify.js';
|
|
42
43
|
import { marketInitCommand, marketPackCommand, marketPublishCommand, marketInstallCommand, marketSearchCommand, marketListCommand, } from './commands/market.js';
|
|
43
44
|
import { mcpSetupCommand } from './commands/mcp-setup.js';
|
|
44
45
|
import { logger } from './utils/logger.js';
|
|
@@ -352,6 +353,71 @@ createCmd
|
|
|
352
353
|
.action(wrapAction(async (name, file, options) => {
|
|
353
354
|
await createNodeCommand(name, file, options);
|
|
354
355
|
}));
|
|
356
|
+
// Modify command (with subcommands)
|
|
357
|
+
const modifyCmd = program.command('modify').description('Modify workflow structure');
|
|
358
|
+
modifyCmd
|
|
359
|
+
.command('addNode')
|
|
360
|
+
.description('Add a node instance to a workflow')
|
|
361
|
+
.requiredOption('--file <path>', 'Workflow file')
|
|
362
|
+
.requiredOption('--nodeId <id>', 'Node instance ID')
|
|
363
|
+
.requiredOption('--nodeType <type>', 'Node type name')
|
|
364
|
+
.action(wrapAction(async (options) => {
|
|
365
|
+
await modifyAddNodeCommand(options.file, options);
|
|
366
|
+
}));
|
|
367
|
+
modifyCmd
|
|
368
|
+
.command('removeNode')
|
|
369
|
+
.description('Remove a node instance from a workflow')
|
|
370
|
+
.requiredOption('--file <path>', 'Workflow file')
|
|
371
|
+
.requiredOption('--nodeId <id>', 'Node instance ID')
|
|
372
|
+
.action(wrapAction(async (options) => {
|
|
373
|
+
await modifyRemoveNodeCommand(options.file, options);
|
|
374
|
+
}));
|
|
375
|
+
modifyCmd
|
|
376
|
+
.command('addConnection')
|
|
377
|
+
.description('Add a connection between nodes')
|
|
378
|
+
.requiredOption('--file <path>', 'Workflow file')
|
|
379
|
+
.requiredOption('--from <node.port>', 'Source (e.g. nodeA.output)')
|
|
380
|
+
.requiredOption('--to <node.port>', 'Target (e.g. nodeB.input)')
|
|
381
|
+
.action(wrapAction(async (options) => {
|
|
382
|
+
await modifyAddConnectionCommand(options.file, options);
|
|
383
|
+
}));
|
|
384
|
+
modifyCmd
|
|
385
|
+
.command('removeConnection')
|
|
386
|
+
.description('Remove a connection between nodes')
|
|
387
|
+
.requiredOption('--file <path>', 'Workflow file')
|
|
388
|
+
.requiredOption('--from <node.port>', 'Source (e.g. nodeA.output)')
|
|
389
|
+
.requiredOption('--to <node.port>', 'Target (e.g. nodeB.input)')
|
|
390
|
+
.action(wrapAction(async (options) => {
|
|
391
|
+
await modifyRemoveConnectionCommand(options.file, options);
|
|
392
|
+
}));
|
|
393
|
+
modifyCmd
|
|
394
|
+
.command('renameNode')
|
|
395
|
+
.description('Rename a node instance (updates all connections)')
|
|
396
|
+
.requiredOption('--file <path>', 'Workflow file')
|
|
397
|
+
.requiredOption('--oldId <id>', 'Current node ID')
|
|
398
|
+
.requiredOption('--newId <id>', 'New node ID')
|
|
399
|
+
.action(wrapAction(async (options) => {
|
|
400
|
+
await modifyRenameNodeCommand(options.file, options);
|
|
401
|
+
}));
|
|
402
|
+
modifyCmd
|
|
403
|
+
.command('setPosition')
|
|
404
|
+
.description('Set position of a node instance')
|
|
405
|
+
.requiredOption('--file <path>', 'Workflow file')
|
|
406
|
+
.requiredOption('--nodeId <id>', 'Node instance ID')
|
|
407
|
+
.requiredOption('--x <number>', 'X coordinate')
|
|
408
|
+
.requiredOption('--y <number>', 'Y coordinate')
|
|
409
|
+
.action(wrapAction(async (options) => {
|
|
410
|
+
await modifySetPositionCommand(options.file, options);
|
|
411
|
+
}));
|
|
412
|
+
modifyCmd
|
|
413
|
+
.command('setLabel')
|
|
414
|
+
.description('Set display label for a node instance')
|
|
415
|
+
.requiredOption('--file <path>', 'Workflow file')
|
|
416
|
+
.requiredOption('--nodeId <id>', 'Node instance ID')
|
|
417
|
+
.requiredOption('--label <text>', 'Display label')
|
|
418
|
+
.action(wrapAction(async (options) => {
|
|
419
|
+
await modifySetLabelCommand(options.file, options);
|
|
420
|
+
}));
|
|
355
421
|
// Templates command
|
|
356
422
|
program
|
|
357
423
|
.command('templates')
|
|
@@ -503,14 +569,19 @@ program
|
|
|
503
569
|
}));
|
|
504
570
|
// Implement command
|
|
505
571
|
program
|
|
506
|
-
.command('implement <input>
|
|
572
|
+
.command('implement <input> [node]')
|
|
507
573
|
.description('Replace a stub node with a real function skeleton')
|
|
508
574
|
.option('-w, --workflow <name>', 'Specific workflow name')
|
|
575
|
+
.option('--nodeId <id>', 'Node to implement (alternative to positional arg)')
|
|
509
576
|
.option('-p, --preview', 'Preview the generated code without writing', false)
|
|
510
577
|
.action(wrapAction(async (input, node, options) => {
|
|
578
|
+
const nodeName = node ?? options.nodeId;
|
|
579
|
+
if (!nodeName) {
|
|
580
|
+
throw new Error('Node name is required (as positional arg or --nodeId flag)');
|
|
581
|
+
}
|
|
511
582
|
if (options.workflow)
|
|
512
583
|
options.workflowName = options.workflow;
|
|
513
|
-
await implementCommand(input,
|
|
584
|
+
await implementCommand(input, nodeName, options);
|
|
514
585
|
}));
|
|
515
586
|
// Changelog command
|
|
516
587
|
program
|
|
@@ -68,9 +68,9 @@ function combineData(dataA: any, dataB: any): { aggregated: any } {
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* @flowWeaver workflow
|
|
71
|
-
* @node sourceA fetchSourceA [position: -180 -90]
|
|
72
|
-
* @node sourceB fetchSourceB [position: -180 90]
|
|
73
|
-
* @node combiner combineData [position: 90 0]
|
|
71
|
+
* @node sourceA fetchSourceA [position: -180 -90] [color: "blue"] [icon: "download"]
|
|
72
|
+
* @node sourceB fetchSourceB [position: -180 90] [color: "blue"] [icon: "download"]
|
|
73
|
+
* @node combiner combineData [position: 90 0] [color: "purple"] [icon: "callMerge"]
|
|
74
74
|
* @position Start -450 0
|
|
75
75
|
* @position Exit 360 0
|
|
76
76
|
* @connect Start.execute -> sourceA.execute
|
|
@@ -283,9 +283,9 @@ async function executeTools(
|
|
|
283
283
|
* AI Agent that uses tools to accomplish tasks
|
|
284
284
|
*
|
|
285
285
|
* @flowWeaver workflow
|
|
286
|
-
* @node loop agentLoop [
|
|
287
|
-
* @node llm callLLM loop.iteration [
|
|
288
|
-
* @node tools executeTools loop.iteration [
|
|
286
|
+
* @node loop agentLoop [position: -180 0] [color: "purple"] [icon: "smartToy"]
|
|
287
|
+
* @node llm callLLM loop.iteration [color: "blue"] [icon: "psychology"]
|
|
288
|
+
* @node tools executeTools loop.iteration [color: "orange"] [icon: "build"] [suppress: "AGENT_UNGUARDED_TOOL_EXECUTOR"]
|
|
289
289
|
* @position Start -450 0
|
|
290
290
|
* @position Exit 360 0
|
|
291
291
|
* @connect Start.execute -> loop.execute
|
|
@@ -39,7 +39,7 @@ Be concise but friendly. Remember context from earlier in the conversation.\`;
|
|
|
39
39
|
* @flowWeaver nodeType
|
|
40
40
|
* @label Memory
|
|
41
41
|
* @input conversationId [order:1] - Conversation identifier
|
|
42
|
-
* @input newMessage [order:2] - Message to add
|
|
42
|
+
* @input [newMessage] [order:2] - Message to add
|
|
43
43
|
* @input [maxHistory=50] [order:3] - Max messages to retain
|
|
44
44
|
* @input execute [order:0] - Execute
|
|
45
45
|
* @output history [order:2] - Conversation history
|
|
@@ -127,9 +127,9 @@ async function chat(
|
|
|
127
127
|
* Stateful chat with conversation memory
|
|
128
128
|
*
|
|
129
129
|
* @flowWeaver workflow
|
|
130
|
-
* @node mem memory [position: -150 0]
|
|
131
|
-
* @node respond chat [position: 50 0]
|
|
132
|
-
* @node saveMem memory [position: 250 0]
|
|
130
|
+
* @node mem memory [position: -150 0] [color: "teal"] [icon: "database"]
|
|
131
|
+
* @node respond chat [position: 50 0] [color: "purple"] [icon: "campaign"] [suppress: "AGENT_LLM_NO_FALLBACK"]
|
|
132
|
+
* @node saveMem memory [position: 250 0] [color: "teal"] [icon: "database"] [suppress: "UNUSED_OUTPUT_PORT"]
|
|
133
133
|
* @position Start -350 0
|
|
134
134
|
* @position Exit 450 0
|
|
135
135
|
* @connect Start.execute -> mem.execute
|
|
@@ -140,6 +140,7 @@ async function chat(
|
|
|
140
140
|
* @connect Start.conversationId -> saveMem.conversationId
|
|
141
141
|
* @connect respond.responseMessage -> saveMem.newMessage
|
|
142
142
|
* @connect respond.onSuccess -> saveMem.execute
|
|
143
|
+
* @connect respond.onFailure -> Exit.onFailure
|
|
143
144
|
* @connect respond.response -> Exit.response
|
|
144
145
|
* @connect saveMem.onSuccess -> Exit.onSuccess
|
|
145
146
|
* @param execute [order:0] - Execute
|
|
@@ -46,7 +46,7 @@ const documentStore: Document[] = [
|
|
|
46
46
|
* @flowWeaver nodeType
|
|
47
47
|
* @label Retrieve
|
|
48
48
|
* @input query [order:1] - Search query
|
|
49
|
-
* @input topK [order:2] - Number of results (default 3)
|
|
49
|
+
* @input [topK] [order:2] - Number of results (default 3)
|
|
50
50
|
* @input execute [order:0] - Execute
|
|
51
51
|
* @output documents [order:2] - Retrieved documents
|
|
52
52
|
* @output context [order:3] - Combined document text
|
|
@@ -145,8 +145,8 @@ Answer:\`;
|
|
|
145
145
|
* RAG Pipeline for knowledge-based Q&A
|
|
146
146
|
*
|
|
147
147
|
* @flowWeaver workflow
|
|
148
|
-
* @node retriever retrieve [position: -50 0]
|
|
149
|
-
* @node generator generate [position: 200 0]
|
|
148
|
+
* @node retriever retrieve [position: -50 0] [color: "teal"] [icon: "search"] [suppress: "UNUSED_OUTPUT_PORT"]
|
|
149
|
+
* @node generator generate [position: 200 0] [color: "purple"] [icon: "autoAwesome"]
|
|
150
150
|
* @position Start -300 0
|
|
151
151
|
* @position Exit 400 0
|
|
152
152
|
* @connect Start.execute -> retriever.execute
|
|
@@ -247,25 +247,25 @@ async function act(
|
|
|
247
247
|
* ReAct Agent — iterative Thought→Action→Observation loop
|
|
248
248
|
*
|
|
249
249
|
* @flowWeaver workflow
|
|
250
|
-
* @node loop reactLoop [
|
|
251
|
-
* @node thinking think loop.step [
|
|
252
|
-
* @node acting act loop.step [
|
|
250
|
+
* @node loop reactLoop [position: -150 0] [color: "purple"] [icon: "psychology"]
|
|
251
|
+
* @node thinking think loop.step [color: "blue"] [icon: "autoAwesome"]
|
|
252
|
+
* @node acting act loop.step [color: "orange"] [icon: "bolt"]
|
|
253
253
|
* @position Start -400 0
|
|
254
254
|
* @position Exit 350 0
|
|
255
255
|
* @connect Start.execute -> loop.execute
|
|
256
256
|
* @connect Start.task -> loop.task
|
|
257
|
-
* @connect loop.start -> thinking.execute
|
|
258
|
-
* @connect loop.messages -> thinking.messages
|
|
257
|
+
* @connect loop.start:step -> thinking.execute
|
|
258
|
+
* @connect loop.messages:step -> thinking.messages
|
|
259
259
|
* @connect thinking.onSuccess -> acting.execute
|
|
260
260
|
* @connect thinking.action -> acting.action
|
|
261
261
|
* @connect thinking.actionInput -> acting.actionInput
|
|
262
|
-
* @connect thinking.thought -> loop.thought
|
|
263
|
-
* @connect thinking.action -> loop.action
|
|
264
|
-
* @connect thinking.actionInput -> loop.actionInput
|
|
265
|
-
* @connect thinking.onFailure -> loop.failure
|
|
266
|
-
* @connect acting.observation -> loop.observation
|
|
267
|
-
* @connect acting.onSuccess -> loop.success
|
|
268
|
-
* @connect acting.onFailure -> loop.failure
|
|
262
|
+
* @connect thinking.thought -> loop.thought:step
|
|
263
|
+
* @connect thinking.action -> loop.action:step
|
|
264
|
+
* @connect thinking.actionInput -> loop.actionInput:step
|
|
265
|
+
* @connect thinking.onFailure -> loop.failure:step
|
|
266
|
+
* @connect acting.observation -> loop.observation:step
|
|
267
|
+
* @connect acting.onSuccess -> loop.success:step
|
|
268
|
+
* @connect acting.onFailure -> loop.failure:step
|
|
269
269
|
* @connect loop.onSuccess -> Exit.onSuccess
|
|
270
270
|
* @connect loop.onFailure -> Exit.onFailure
|
|
271
271
|
* @connect loop.answer -> Exit.answer
|
|
@@ -105,9 +105,9 @@ function handleFailure(
|
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
107
|
* @flowWeaver workflow
|
|
108
|
-
* @node router evaluateCondition [position: -180 0]
|
|
109
|
-
* @node successHandler handleSuccess [position: 90 -90]
|
|
110
|
-
* @node failureHandler handleFailure [position: 90 90]
|
|
108
|
+
* @node router evaluateCondition [position: -180 0] [color: "orange"] [icon: "altRoute"] [suppress: "UNUSED_OUTPUT_PORT"]
|
|
109
|
+
* @node successHandler handleSuccess [position: 90 -90] [color: "green"] [icon: "checkCircle"]
|
|
110
|
+
* @node failureHandler handleFailure [position: 90 90] [color: "red"] [icon: "error"]
|
|
111
111
|
* @position Start -450 0
|
|
112
112
|
* @position Exit 360 0
|
|
113
113
|
* @connect Start.execute -> router.execute
|
|
@@ -105,8 +105,8 @@ function tryOperation(
|
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
107
|
* @flowWeaver workflow
|
|
108
|
-
* @node loop retryLoop [
|
|
109
|
-
* @node tryOp tryOperation loop.attempt [
|
|
108
|
+
* @node loop retryLoop [position: -90 0] [color: "orange"] [icon: "refresh"]
|
|
109
|
+
* @node tryOp tryOperation loop.attempt [color: "blue"] [icon: "playArrow"]
|
|
110
110
|
* @position Start -450 0
|
|
111
111
|
* @position Exit 360 0
|
|
112
112
|
* @connect Start.execute -> loop.execute
|
|
@@ -101,9 +101,9 @@ function aggregateResults(
|
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
103
|
* @flowWeaver workflow
|
|
104
|
-
* @node iterator forEachItem [
|
|
105
|
-
* @node processor processItem iterator.processItem [
|
|
106
|
-
* @node aggregator aggregateResults [position: 270 0]
|
|
104
|
+
* @node iterator forEachItem [position: -90 0] [color: "purple"] [icon: "repeat"]
|
|
105
|
+
* @node processor processItem iterator.processItem [color: "blue"] [icon: "settings"]
|
|
106
|
+
* @node aggregator aggregateResults [position: 270 0] [color: "teal"] [icon: "inventory"]
|
|
107
107
|
* @position Start -450 0
|
|
108
108
|
* @position Exit 450 0
|
|
109
109
|
* @connect Start.execute -> iterator.execute
|
|
@@ -87,12 +87,13 @@ function outputResult(data: any): { result: any } {
|
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
89
|
* @flowWeaver workflow
|
|
90
|
-
* @node validator validateData [position: -300 0]
|
|
91
|
-
* @node transformer transformData [position: 0 0]
|
|
92
|
-
* @node outputter outputResult [position: 300 0]
|
|
90
|
+
* @node validator validateData [position: -300 0] [color: "green"] [icon: "verified"] [suppress: "UNUSED_OUTPUT_PORT"]
|
|
91
|
+
* @node transformer transformData [position: 0 0] [color: "blue"] [icon: "sync"]
|
|
92
|
+
* @node outputter outputResult [position: 300 0] [color: "cyan"] [icon: "inventory"]
|
|
93
93
|
* @position Start -600 0
|
|
94
94
|
* @position Exit 600 0
|
|
95
95
|
* @path Start -> validator -> transformer -> outputter -> Exit
|
|
96
|
+
* @connect outputter.result -> Exit.result
|
|
96
97
|
* @connect validator.onFailure -> Exit.onFailure
|
|
97
98
|
* @connect validator.error -> Exit.error
|
|
98
99
|
* @param execute [order:0] - Execute
|
|
@@ -134,9 +135,11 @@ function ${name}(${inputPortName}: any): { ${outputPortName}: any } {
|
|
|
134
135
|
// Generate workflow annotations
|
|
135
136
|
const spacing = 300;
|
|
136
137
|
const startX = -(nodeNames.length * (spacing / 2) + spacing / 2);
|
|
138
|
+
const stepColors = ['green', 'blue', 'cyan', 'orange', 'purple', 'teal', 'pink', 'yellow'];
|
|
137
139
|
const nodeAnnotations = nodeNames.map((name, i) => {
|
|
138
140
|
const x = startX + (i + 1) * spacing;
|
|
139
|
-
|
|
141
|
+
const color = stepColors[i % stepColors.length];
|
|
142
|
+
return ` * @node step${i} ${name} [position: ${x} 0] [color: "${color}"] [icon: "settings"]`;
|
|
140
143
|
}).join('\n');
|
|
141
144
|
const positionAnnotations = [
|
|
142
145
|
` * @position Start ${startX} 0`,
|
|
@@ -120,9 +120,9 @@ function formatResponse(
|
|
|
120
120
|
|
|
121
121
|
/**
|
|
122
122
|
* @flowWeaver workflow
|
|
123
|
-
* @node validator validateRequest [position: -180 0]
|
|
124
|
-
* @node processor processPayload [position: 90 -60]
|
|
125
|
-
* @node responder formatResponse [position: 270 0]
|
|
123
|
+
* @node validator validateRequest [position: -180 0] [color: "green"] [icon: "verified"]
|
|
124
|
+
* @node processor processPayload [position: 90 -60] [color: "blue"] [icon: "settings"]
|
|
125
|
+
* @node responder formatResponse [position: 270 0] [color: "cyan"] [icon: "send"]
|
|
126
126
|
* @position Start -450 0
|
|
127
127
|
* @position Exit 450 0
|
|
128
128
|
* @connect Start.execute -> validator.execute
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.20.
|
|
1
|
+
export declare const VERSION = "0.20.3";
|
|
2
2
|
//# sourceMappingURL=generated-version.d.ts.map
|
package/dist/jsdoc-parser.d.ts
CHANGED
package/dist/jsdoc-parser.js
CHANGED
|
@@ -740,7 +740,7 @@ export class JSDocParser {
|
|
|
740
740
|
else if (func) {
|
|
741
741
|
const returnType = func.getReturnType();
|
|
742
742
|
const returnTypeText = returnType.getText();
|
|
743
|
-
const fieldMatch = returnTypeText.match(new RegExp(`${name}
|
|
743
|
+
const fieldMatch = returnTypeText.match(new RegExp(`${name}\\??\\s*:\\s*([^;},]+)`));
|
|
744
744
|
if (fieldMatch) {
|
|
745
745
|
type = inferDataTypeFromTS(fieldMatch[1].trim());
|
|
746
746
|
}
|
|
@@ -795,7 +795,7 @@ export class JSDocParser {
|
|
|
795
795
|
paramTypeText === '{}' ||
|
|
796
796
|
/^\{\s*\[[\w]+:\s*string\]:\s*(never|any|unknown);\s*\}$/.test(paramTypeText));
|
|
797
797
|
if (!isCatchAllRecord) {
|
|
798
|
-
const fieldMatch = paramTypeText.match(new RegExp(`${name}
|
|
798
|
+
const fieldMatch = paramTypeText.match(new RegExp(`${name}\\??\\s*:\\s*([^;},]+)`));
|
|
799
799
|
if (fieldMatch) {
|
|
800
800
|
type = inferDataTypeFromTS(fieldMatch[1].trim());
|
|
801
801
|
}
|
|
@@ -850,7 +850,7 @@ export class JSDocParser {
|
|
|
850
850
|
if (!result) {
|
|
851
851
|
return;
|
|
852
852
|
}
|
|
853
|
-
const { instanceId, nodeType, parentScope, label, expressions, portOrder, portLabel, minimized, pullExecution, size, position, color, icon, tags, job, environment, } = result;
|
|
853
|
+
const { instanceId, nodeType, parentScope, label, expressions, portOrder, portLabel, minimized, pullExecution, size, position, color, icon, tags, job, environment, suppress, } = result;
|
|
854
854
|
// Capture source location from tag
|
|
855
855
|
const line = tag.getStartLineNumber();
|
|
856
856
|
// Build portConfigs from portOrder, portLabel, and expressions
|
|
@@ -900,6 +900,7 @@ export class JSDocParser {
|
|
|
900
900
|
...(position && { x: position.x, y: position.y }),
|
|
901
901
|
...(job && { job }),
|
|
902
902
|
...(environment && { environment }),
|
|
903
|
+
...(suppress && suppress.length > 0 && { suppressWarnings: suppress }),
|
|
903
904
|
sourceLocation: { line, column: 0 },
|
|
904
905
|
});
|
|
905
906
|
}
|
|
@@ -7,191 +7,12 @@ import { listPatterns, applyPattern, findWorkflows, extractPattern } from '../ap
|
|
|
7
7
|
import { generateInPlace } from '../api/generate-in-place.js';
|
|
8
8
|
import { applyMigrations, getRegisteredMigrations } from '../migration/registry.js';
|
|
9
9
|
import { describeWorkflow, formatDescribeOutput } from '../cli/commands/describe.js';
|
|
10
|
+
import { applyModifyOperation, validateModifyParams } from '../api/modify-operation.js';
|
|
10
11
|
import { addNode as manipAddNode, removeNode as manipRemoveNode, renameNode as manipRenameNode, addConnection as manipAddConnection, removeConnection as manipRemoveConnection, setNodePosition as manipSetNodePosition, setNodeLabel as manipSetNodeLabel, } from '../api/manipulation/index.js';
|
|
11
12
|
import { findIsolatedNodes } from '../api/query.js';
|
|
12
13
|
import { AnnotationParser } from '../parser.js';
|
|
13
14
|
import { makeToolResult, makeErrorResult, addHintsToItems } from './response-utils.js';
|
|
14
15
|
import { getFriendlyError } from '../friendly-errors.js';
|
|
15
|
-
// Runtime validation schemas for fw_modify operations
|
|
16
|
-
const modifyParamsSchemas = {
|
|
17
|
-
addNode: z.object({
|
|
18
|
-
nodeId: z.string({ required_error: 'nodeId is required' }),
|
|
19
|
-
nodeType: z.string({ required_error: 'nodeType is required' }),
|
|
20
|
-
x: z.number().optional(),
|
|
21
|
-
y: z.number().optional(),
|
|
22
|
-
}),
|
|
23
|
-
removeNode: z.object({
|
|
24
|
-
nodeId: z.string({ required_error: 'nodeId is required' }),
|
|
25
|
-
}),
|
|
26
|
-
renameNode: z.object({
|
|
27
|
-
oldId: z.string({ required_error: 'oldId is required' }),
|
|
28
|
-
newId: z.string({ required_error: 'newId is required' }),
|
|
29
|
-
}),
|
|
30
|
-
addConnection: z.object({
|
|
31
|
-
from: z.string({ required_error: 'from is required (format: "node.port")' }),
|
|
32
|
-
to: z.string({ required_error: 'to is required (format: "node.port")' }),
|
|
33
|
-
}),
|
|
34
|
-
removeConnection: z.object({
|
|
35
|
-
from: z.string({ required_error: 'from is required (format: "node.port")' }),
|
|
36
|
-
to: z.string({ required_error: 'to is required (format: "node.port")' }),
|
|
37
|
-
}),
|
|
38
|
-
setNodePosition: z.object({
|
|
39
|
-
nodeId: z.string({ required_error: 'nodeId is required' }),
|
|
40
|
-
x: z.number({ required_error: 'x is required', invalid_type_error: 'x must be a number' }),
|
|
41
|
-
y: z.number({ required_error: 'y is required', invalid_type_error: 'y must be a number' }),
|
|
42
|
-
}),
|
|
43
|
-
setNodeLabel: z.object({
|
|
44
|
-
nodeId: z.string({ required_error: 'nodeId is required' }),
|
|
45
|
-
label: z.string({ required_error: 'label is required' }),
|
|
46
|
-
}),
|
|
47
|
-
};
|
|
48
|
-
function validateModifyParams(operation, params) {
|
|
49
|
-
const schema = modifyParamsSchemas[operation];
|
|
50
|
-
if (!schema) {
|
|
51
|
-
return { success: false, error: `Unknown operation: ${operation}` };
|
|
52
|
-
}
|
|
53
|
-
const result = schema.safeParse(params);
|
|
54
|
-
if (!result.success) {
|
|
55
|
-
const messages = result.error.issues.map((i) => i.message).join('; ');
|
|
56
|
-
return { success: false, error: `${operation} params invalid: ${messages}` };
|
|
57
|
-
}
|
|
58
|
-
return { success: true };
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Apply a single modify operation to an AST.
|
|
62
|
-
* Shared by fw_modify and fw_modify_batch.
|
|
63
|
-
*/
|
|
64
|
-
function applyModifyOperation(ast, operation, params) {
|
|
65
|
-
const p = params;
|
|
66
|
-
const warnings = [];
|
|
67
|
-
const extraData = {};
|
|
68
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- AST manipulation functions use loose typing
|
|
69
|
-
let modifiedAST = ast;
|
|
70
|
-
switch (operation) {
|
|
71
|
-
case 'addNode': {
|
|
72
|
-
const nodeId = p.nodeId;
|
|
73
|
-
const nodeType = p.nodeType;
|
|
74
|
-
const nodeTypeExists = modifiedAST.nodeTypes.some((nt) => nt.name === nodeType || nt.functionName === nodeType);
|
|
75
|
-
if (!nodeTypeExists) {
|
|
76
|
-
warnings.push(`Node type "${nodeType}" is not defined in the file. ` +
|
|
77
|
-
`The node will be added but may not render until the type is defined.`);
|
|
78
|
-
}
|
|
79
|
-
let autoX = typeof p.x === 'number' ? p.x : undefined;
|
|
80
|
-
let autoY = typeof p.y === 'number' ? p.y : undefined;
|
|
81
|
-
if (autoX === undefined || autoY === undefined) {
|
|
82
|
-
const positions = modifiedAST.instances
|
|
83
|
-
.map((inst) => inst.config)
|
|
84
|
-
.filter((c) => c !== undefined &&
|
|
85
|
-
c !== null &&
|
|
86
|
-
typeof c.x === 'number' &&
|
|
87
|
-
typeof c.y === 'number');
|
|
88
|
-
if (positions.length > 0) {
|
|
89
|
-
const maxX = Math.max(...positions.map((pos) => pos.x));
|
|
90
|
-
if (autoX === undefined)
|
|
91
|
-
autoX = maxX + 180;
|
|
92
|
-
if (autoY === undefined)
|
|
93
|
-
autoY = 0;
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
if (autoX === undefined)
|
|
97
|
-
autoX = 0;
|
|
98
|
-
if (autoY === undefined)
|
|
99
|
-
autoY = 0;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
modifiedAST = manipAddNode(modifiedAST, {
|
|
103
|
-
type: 'NodeInstance',
|
|
104
|
-
id: nodeId,
|
|
105
|
-
nodeType,
|
|
106
|
-
config: { x: autoX, y: autoY },
|
|
107
|
-
});
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
case 'removeNode': {
|
|
111
|
-
const nodeId = p.nodeId;
|
|
112
|
-
const removedConnections = modifiedAST.connections
|
|
113
|
-
.filter((c) => c.from.node === nodeId || c.to.node === nodeId)
|
|
114
|
-
.map((c) => ({
|
|
115
|
-
from: `${c.from.node}.${c.from.port}`,
|
|
116
|
-
to: `${c.to.node}.${c.to.port}`,
|
|
117
|
-
}));
|
|
118
|
-
modifiedAST = manipRemoveNode(modifiedAST, nodeId);
|
|
119
|
-
if (removedConnections.length > 0) {
|
|
120
|
-
extraData.removedConnections = removedConnections;
|
|
121
|
-
}
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
case 'renameNode': {
|
|
125
|
-
modifiedAST = manipRenameNode(modifiedAST, p.oldId, p.newId);
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
case 'addConnection': {
|
|
129
|
-
const from = p.from;
|
|
130
|
-
const to = p.to;
|
|
131
|
-
const [fromNode, fromPort] = from.split('.');
|
|
132
|
-
const [toNode, toPort] = to.split('.');
|
|
133
|
-
if (!fromPort || !toPort) {
|
|
134
|
-
throw new Error('Connection format must be "node.port" (e.g., "Start.execute")');
|
|
135
|
-
}
|
|
136
|
-
const validNodes = [
|
|
137
|
-
'Start',
|
|
138
|
-
'Exit',
|
|
139
|
-
...modifiedAST.instances.map((i) => i.id),
|
|
140
|
-
];
|
|
141
|
-
if (!validNodes.includes(fromNode)) {
|
|
142
|
-
throw new Error(`Source node "${fromNode}" not found. Available: ${validNodes.join(', ')}`);
|
|
143
|
-
}
|
|
144
|
-
if (!validNodes.includes(toNode)) {
|
|
145
|
-
throw new Error(`Target node "${toNode}" not found. Available: ${validNodes.join(', ')}`);
|
|
146
|
-
}
|
|
147
|
-
if (fromNode !== 'Start' && fromNode !== 'Exit') {
|
|
148
|
-
const inst = modifiedAST.instances.find((i) => i.id === fromNode);
|
|
149
|
-
const nt = modifiedAST.nodeTypes.find((t) => t.name === inst?.nodeType);
|
|
150
|
-
if (nt && !nt.outputs[fromPort]) {
|
|
151
|
-
throw new Error(`Node "${fromNode}" has no output "${fromPort}". Available: ${Object.keys(nt.outputs).join(', ')}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
if (toNode !== 'Start' && toNode !== 'Exit') {
|
|
155
|
-
const inst = modifiedAST.instances.find((i) => i.id === toNode);
|
|
156
|
-
const nt = modifiedAST.nodeTypes.find((t) => t.name === inst?.nodeType);
|
|
157
|
-
if (nt && !nt.inputs[toPort]) {
|
|
158
|
-
throw new Error(`Node "${toNode}" has no input "${toPort}". Available: ${Object.keys(nt.inputs).join(', ')}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
modifiedAST = manipAddConnection(modifiedAST, from, to);
|
|
162
|
-
// Transition from autoConnect to explicit mode when connections are manually modified
|
|
163
|
-
if (modifiedAST.options?.autoConnect) {
|
|
164
|
-
modifiedAST = { ...modifiedAST, options: { ...modifiedAST.options, autoConnect: undefined } };
|
|
165
|
-
warnings.push('autoConnect was disabled because connections were manually modified');
|
|
166
|
-
}
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
case 'removeConnection': {
|
|
170
|
-
modifiedAST = manipRemoveConnection(modifiedAST, p.from, p.to);
|
|
171
|
-
// Transition from autoConnect to explicit mode when connections are manually modified
|
|
172
|
-
if (modifiedAST.options?.autoConnect) {
|
|
173
|
-
modifiedAST = { ...modifiedAST, options: { ...modifiedAST.options, autoConnect: undefined } };
|
|
174
|
-
warnings.push('autoConnect was disabled because connections were manually modified');
|
|
175
|
-
}
|
|
176
|
-
const newlyIsolated = findIsolatedNodes(modifiedAST);
|
|
177
|
-
if (newlyIsolated.length > 0) {
|
|
178
|
-
extraData.newlyIsolatedNodes = newlyIsolated;
|
|
179
|
-
}
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
case 'setNodePosition': {
|
|
183
|
-
modifiedAST = manipSetNodePosition(modifiedAST, p.nodeId, p.x, p.y);
|
|
184
|
-
break;
|
|
185
|
-
}
|
|
186
|
-
case 'setNodeLabel': {
|
|
187
|
-
modifiedAST = manipSetNodeLabel(modifiedAST, p.nodeId, p.label);
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
default:
|
|
191
|
-
throw new Error(`Unknown operation: ${operation}`);
|
|
192
|
-
}
|
|
193
|
-
return { ast: modifiedAST, warnings, extraData };
|
|
194
|
-
}
|
|
195
16
|
export function registerPatternTools(mcp) {
|
|
196
17
|
mcp.tool('fw_list_patterns', 'List reusable patterns defined in a file.', {
|
|
197
18
|
filePath: z.string().describe('Path to file containing patterns'),
|
package/dist/parser.js
CHANGED
|
@@ -970,6 +970,7 @@ export class AnnotationParser {
|
|
|
970
970
|
...(inst.tags && inst.tags.length > 0 && { tags: inst.tags }),
|
|
971
971
|
...(inst.width && { width: inst.width }),
|
|
972
972
|
...(inst.height && { height: inst.height }),
|
|
973
|
+
...(inst.suppressWarnings?.length && { suppressWarnings: inst.suppressWarnings }),
|
|
973
974
|
},
|
|
974
975
|
...(inst.sourceLocation && {
|
|
975
976
|
sourceLocation: { file: filePath, ...inst.sourceLocation },
|
package/dist/validator.js
CHANGED
|
@@ -228,6 +228,21 @@ export class WorkflowValidator {
|
|
|
228
228
|
});
|
|
229
229
|
this.warnings.push(...promoted);
|
|
230
230
|
}
|
|
231
|
+
// Filter out warnings suppressed by per-instance [suppress: "CODE"] annotations
|
|
232
|
+
const suppressMap = new Map();
|
|
233
|
+
for (const inst of workflow.instances) {
|
|
234
|
+
if (inst.config?.suppressWarnings?.length) {
|
|
235
|
+
suppressMap.set(inst.id, new Set(inst.config.suppressWarnings));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (suppressMap.size > 0) {
|
|
239
|
+
this.warnings = this.warnings.filter((w) => {
|
|
240
|
+
if (!w.node)
|
|
241
|
+
return true;
|
|
242
|
+
const codes = suppressMap.get(w.node);
|
|
243
|
+
return !codes || !codes.has(w.code);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
231
246
|
// Attach doc URLs to diagnostics that have mapped error codes
|
|
232
247
|
for (const diag of [...this.errors, ...this.warnings]) {
|
|
233
248
|
if (!diag.docUrl && ERROR_DOC_URLS[diag.code]) {
|
|
@@ -423,6 +423,17 @@ Visual tags/badges on the instance. Each tag has a label string and optional too
|
|
|
423
423
|
@node myNode MyType [tags: "async" "Runs asynchronously", "beta"]
|
|
424
424
|
```
|
|
425
425
|
|
|
426
|
+
### Suppress Warnings (`[suppress: ...]`)
|
|
427
|
+
|
|
428
|
+
Silences specific validator warnings on a per-instance basis. Useful when a warning is intentional, such as output ports that are deliberately left unconnected in CI/CD workflows where data is discarded by design.
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
@node fetch fetchData [suppress: "UNUSED_OUTPUT_PORT"]
|
|
432
|
+
@node check runCheck [suppress: "UNUSED_OUTPUT_PORT", "UNREACHABLE_EXIT_PORT"]
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
The suppression is scoped to the annotated instance only. Other instances of the same type still produce warnings normally. The codes correspond to the warning codes listed in the error codes reference.
|
|
436
|
+
|
|
426
437
|
### Combining Attributes
|
|
427
438
|
|
|
428
439
|
Multiple attribute brackets can appear on the same `@node`:
|