@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.
Files changed (38) hide show
  1. package/dist/annotation-generator.js +7 -1
  2. package/dist/api/index.d.ts +1 -0
  3. package/dist/api/index.js +1 -0
  4. package/dist/api/modify-operation.d.ts +15 -0
  5. package/dist/api/modify-operation.js +177 -0
  6. package/dist/api/validate.js +17 -0
  7. package/dist/ast/types.d.ts +2 -0
  8. package/dist/chevrotain-parser/node-parser.d.ts +2 -0
  9. package/dist/chevrotain-parser/node-parser.js +27 -1
  10. package/dist/chevrotain-parser/tokens.d.ts +1 -0
  11. package/dist/chevrotain-parser/tokens.js +5 -0
  12. package/dist/cli/commands/modify.d.ts +29 -0
  13. package/dist/cli/commands/modify.js +57 -0
  14. package/dist/cli/flow-weaver.mjs +44885 -44640
  15. package/dist/cli/index.js +73 -2
  16. package/dist/cli/templates/workflows/aggregator.js +3 -3
  17. package/dist/cli/templates/workflows/ai-agent.js +3 -3
  18. package/dist/cli/templates/workflows/ai-chat.js +5 -4
  19. package/dist/cli/templates/workflows/ai-rag.js +3 -3
  20. package/dist/cli/templates/workflows/ai-react.js +12 -12
  21. package/dist/cli/templates/workflows/conditional.js +3 -3
  22. package/dist/cli/templates/workflows/error-handler.js +2 -2
  23. package/dist/cli/templates/workflows/foreach.js +3 -3
  24. package/dist/cli/templates/workflows/sequential.js +7 -4
  25. package/dist/cli/templates/workflows/webhook.js +3 -3
  26. package/dist/generated-version.d.ts +1 -1
  27. package/dist/generated-version.js +1 -1
  28. package/dist/jsdoc-parser.d.ts +1 -0
  29. package/dist/jsdoc-parser.js +4 -3
  30. package/dist/mcp/tools-pattern.js +1 -180
  31. package/dist/parser.js +1 -0
  32. package/dist/validator.js +15 -0
  33. package/docs/reference/advanced-annotations.md +11 -0
  34. package/docs/reference/cli-reference.md +118 -1
  35. package/docs/reference/error-codes.md +1 -1
  36. package/docs/reference/jsdoc-grammar.md +4 -2
  37. package/docs/reference/marketplace.md +74 -0
  38. 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> <node>')
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, node, options);
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 [size: 450 350] [position: -180 0]
287
- * @node llm callLLM loop.iteration [position: -40 100]
288
- * @node tools executeTools loop.iteration [position: 120 200]
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 [size: 450 250] [position: -150 0]
251
- * @node thinking think loop.step [position: -80 30]
252
- * @node acting act loop.step [position: 130 30]
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 [size: 300 200] [position: -90 0]
109
- * @node tryOp tryOperation loop.attempt [position: 90 0]
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 [size: 300 200] [position: -90 0]
105
- * @node processor processItem iterator.processItem [position: 90 0]
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
- return ` * @node step${i} ${name} [position: ${x} 0]`;
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";
1
+ export declare const VERSION = "0.20.3";
2
2
  //# sourceMappingURL=generated-version.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by scripts/generate-version.ts — do not edit manually
2
- export const VERSION = '0.20.1';
2
+ export const VERSION = '0.20.3';
3
3
  //# sourceMappingURL=generated-version.js.map
@@ -88,6 +88,7 @@ export interface JSDocWorkflowConfig {
88
88
  job?: string;
89
89
  /** CI/CD deployment environment */
90
90
  environment?: string;
91
+ suppressWarnings?: string[];
91
92
  }>;
92
93
  connections?: Array<{
93
94
  from: {
@@ -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}\\s*:\\s*([^;},]+)`));
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}\\s*:\\s*([^;},]+)`));
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`: