@synergenius/flow-weaver 0.20.0 → 0.20.2

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/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
@@ -9,18 +9,8 @@
9
9
  * when the user actually invokes a pack command.
10
10
  */
11
11
  import * as path from 'path';
12
- import { createRequire } from 'node:module';
13
12
  import { listInstalledPackages } from '../marketplace/registry.js';
14
- function getEngineVersion() {
15
- try {
16
- const req = createRequire(import.meta.url);
17
- const pkg = req('../../package.json');
18
- return pkg.version;
19
- }
20
- catch {
21
- return '0.0.0';
22
- }
23
- }
13
+ import { VERSION } from '../generated-version.js';
24
14
  function compareVersions(a, b) {
25
15
  const pa = a.split('.').map(Number);
26
16
  const pb = b.split('.').map(Number);
@@ -36,9 +26,7 @@ function checkPackEngineVersion(pkg) {
36
26
  if (!required)
37
27
  return;
38
28
  const minVersion = required.replace(/^>=?\s*/, '');
39
- const current = getEngineVersion();
40
- if (current === '0.0.0')
41
- return; // dev mode, skip check
29
+ const current = VERSION;
42
30
  if (compareVersions(current, minVersion) < 0) {
43
31
  console.warn(`\x1b[33mWarning: ${pkg.name} requires flow-weaver >=${minVersion} but ${current} is installed.\x1b[0m`);
44
32
  console.warn(`\x1b[33mRun: npm install @synergenius/flow-weaver@latest\x1b[0m`);
@@ -699,8 +699,8 @@ export function buildDiagramGraph(ast, options = {}) {
699
699
  // Build diagram nodes
700
700
  const diagramNodes = new Map();
701
701
  // Start node — ensure mandatory execute STEP port exists
702
- const allStartPorts = { ...ast.startPorts };
703
- if (!allStartPorts.execute) {
702
+ const allStartPorts = filterHiddenPorts({ ...ast.startPorts });
703
+ if (!allStartPorts.execute && !ast.startPorts.execute?.hidden) {
704
704
  allStartPorts.execute = { dataType: 'STEP' };
705
705
  }
706
706
  const startOutputs = orderedPorts(allStartPorts, 'OUTPUT');
@@ -717,14 +717,14 @@ export function buildDiagramGraph(ast, options = {}) {
717
717
  height: NODE_MIN_HEIGHT,
718
718
  });
719
719
  // Exit node — ensure mandatory onSuccess/onFailure STEP ports exist
720
- const allExitPorts = { ...ast.exitPorts };
721
- if (!allExitPorts.onSuccess) {
720
+ const allExitPorts = filterHiddenPorts({ ...ast.exitPorts });
721
+ if (!allExitPorts.onSuccess && !ast.exitPorts.onSuccess?.hidden) {
722
722
  allExitPorts.onSuccess = { dataType: 'STEP', isControlFlow: true };
723
723
  }
724
- if (!allExitPorts.onFailure) {
724
+ if (!allExitPorts.onFailure && !ast.exitPorts.onFailure?.hidden) {
725
725
  allExitPorts.onFailure = { dataType: 'STEP', isControlFlow: true, failure: true };
726
726
  }
727
- else {
727
+ else if (allExitPorts.onFailure) {
728
728
  allExitPorts.onFailure = { ...allExitPorts.onFailure, failure: true };
729
729
  }
730
730
  const exitInputs = orderedPorts(allExitPorts, 'INPUT');
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.20.0";
1
+ export declare const VERSION = "0.20.2";
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.0';
2
+ export const VERSION = '0.20.2';
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: {
@@ -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
  }
@@ -5,18 +5,8 @@
5
5
  * imports the entrypoint and calls its registerMcpTools(mcp) function.
6
6
  */
7
7
  import * as path from 'path';
8
- import { createRequire } from 'node:module';
9
8
  import { listInstalledPackages } from '../marketplace/registry.js';
10
- function getEngineVersion() {
11
- try {
12
- const req = createRequire(import.meta.url);
13
- const pkg = req('../../package.json');
14
- return pkg.version;
15
- }
16
- catch {
17
- return '0.0.0';
18
- }
19
- }
9
+ import { VERSION } from '../generated-version.js';
20
10
  function compareVersions(a, b) {
21
11
  const pa = a.split('.').map(Number);
22
12
  const pb = b.split('.').map(Number);
@@ -32,9 +22,7 @@ function checkPackEngineVersion(pkg) {
32
22
  if (!required)
33
23
  return;
34
24
  const minVersion = required.replace(/^>=?\s*/, '');
35
- const current = getEngineVersion();
36
- if (current === '0.0.0')
37
- return;
25
+ const current = VERSION;
38
26
  if (compareVersions(current, minVersion) < 0) {
39
27
  process.stderr.write(`\x1b[33mWarning: ${pkg.name} requires flow-weaver >=${minVersion} but ${current} is installed.\x1b[0m\n`);
40
28
  process.stderr.write(`\x1b[33mRun: npm install @synergenius/flow-weaver@latest\x1b[0m\n`);
@@ -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`:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: CLI Reference
3
3
  description: Complete reference for all Flow Weaver CLI commands, flags, and options
4
- keywords: [cli, commands, compile, validate, strip, run, watch, dev, serve, export, diagram, diff, doctor, init, migrate, marketplace, plugin, grammar, changelog, openapi, pattern, create, templates, context]
4
+ keywords: [cli, commands, compile, validate, strip, run, watch, dev, serve, export, diagram, diff, doctor, init, migrate, marketplace, plugin, grammar, changelog, openapi, pattern, create, templates, context, modify, implement, status]
5
5
  ---
6
6
 
7
7
  # CLI Reference
@@ -34,6 +34,9 @@ Complete reference for all `flow-weaver` CLI commands.
34
34
  | `changelog` | Generate changelog from git |
35
35
  | `market` | Marketplace packages |
36
36
  | `plugin` | External plugins |
37
+ | `modify` | Add/remove/rename nodes, connections, positions, and labels |
38
+ | `implement` | Replace stub node with function skeleton |
39
+ | `status` | Report implementation progress |
37
40
  | `context` | Generate LLM context bundle |
38
41
  | `docs` | Browse reference documentation |
39
42
  | `ui` | Send commands to the editor |
@@ -475,6 +478,120 @@ flow-weaver create node checker my-workflow.ts --template validator
475
478
 
476
479
  ---
477
480
 
481
+ ### modify
482
+
483
+ Modify workflow structure programmatically. Parses the file, applies the operation, and regenerates the JSDoc annotations in place. Useful for scripting, CI pipelines, and the genesis self-evolution system.
484
+
485
+ #### modify addNode
486
+
487
+ ```bash
488
+ flow-weaver modify addNode --file <path> --nodeId <id> --nodeType <type>
489
+ ```
490
+
491
+ Adds a new node instance to the workflow. Auto-positions to the right of the rightmost existing node. Warns if the node type isn't defined in the file.
492
+
493
+ #### modify removeNode
494
+
495
+ ```bash
496
+ flow-weaver modify removeNode --file <path> --nodeId <id>
497
+ ```
498
+
499
+ Removes a node instance and all connections attached to it.
500
+
501
+ #### modify addConnection
502
+
503
+ ```bash
504
+ flow-weaver modify addConnection --file <path> --from <node.port> --to <node.port>
505
+ ```
506
+
507
+ Adds a connection between two ports. Both nodes must exist. Port names are validated against the node type definition when available.
508
+
509
+ #### modify removeConnection
510
+
511
+ ```bash
512
+ flow-weaver modify removeConnection --file <path> --from <node.port> --to <node.port>
513
+ ```
514
+
515
+ Removes an existing connection.
516
+
517
+ #### modify renameNode
518
+
519
+ ```bash
520
+ flow-weaver modify renameNode --file <path> --oldId <id> --newId <id>
521
+ ```
522
+
523
+ Renames a node instance and updates all connections that reference it.
524
+
525
+ #### modify setPosition
526
+
527
+ ```bash
528
+ flow-weaver modify setPosition --file <path> --nodeId <id> --x <number> --y <number>
529
+ ```
530
+
531
+ Sets the canvas position of a node instance.
532
+
533
+ #### modify setLabel
534
+
535
+ ```bash
536
+ flow-weaver modify setLabel --file <path> --nodeId <id> --label <text>
537
+ ```
538
+
539
+ Sets the display label for a node instance.
540
+
541
+ **Examples:**
542
+ ```bash
543
+ flow-weaver modify addNode --file workflow.ts --nodeId validator --nodeType validateInput
544
+ flow-weaver modify addConnection --file workflow.ts --from Start.data --to validator.input
545
+ flow-weaver modify removeNode --file workflow.ts --nodeId oldStep
546
+ flow-weaver modify removeConnection --file workflow.ts --from a.output --to b.input
547
+ flow-weaver modify renameNode --file workflow.ts --oldId step1 --newId validateStep
548
+ flow-weaver modify setPosition --file workflow.ts --nodeId step1 --x 200 --y 100
549
+ flow-weaver modify setLabel --file workflow.ts --nodeId step1 --label "Validate Input"
550
+ ```
551
+
552
+ ---
553
+
554
+ ### implement
555
+
556
+ Replace a stub node (`declare function`) with a real function skeleton containing the correct signature, JSDoc annotations, and return type.
557
+
558
+ ```bash
559
+ flow-weaver implement <input> <node> [options]
560
+ flow-weaver implement <input> --nodeId <id> [options]
561
+ ```
562
+
563
+ The node can be specified as a positional argument or with the `--nodeId` flag.
564
+
565
+ | Flag | Description | Default |
566
+ |------|-------------|---------|
567
+ | `-w, --workflow <name>` | Specific workflow name | — |
568
+ | `--nodeId <id>` | Node to implement (alternative to positional arg) | — |
569
+ | `-p, --preview` | Preview without writing | `false` |
570
+
571
+ **Examples:**
572
+ ```bash
573
+ flow-weaver implement workflow.ts validateInput
574
+ flow-weaver implement workflow.ts --nodeId validateInput
575
+ flow-weaver implement workflow.ts myNode --preview
576
+ ```
577
+
578
+ ---
579
+
580
+ ### status
581
+
582
+ Report implementation progress for stub workflows. Shows which nodes are implemented vs still declared as stubs.
583
+
584
+ ```bash
585
+ flow-weaver status <input> [options]
586
+ ```
587
+
588
+ | Flag | Description | Default |
589
+ |------|-------------|---------|
590
+ | `-w, --workflow <name>` | Specific workflow name | — |
591
+ | `--json` | Output as JSON | `false` |
592
+
593
+ ---
594
+
478
595
  ### templates
479
596
 
480
597
  List available workflow and node templates.
@@ -540,7 +540,7 @@ const fetchData = (execute: boolean, url: string, apiKey: string) => { ... };
540
540
  | Severity | Warning |
541
541
  | Meaning | A node's data output port is never connected to anything. The data produced by this port is discarded. |
542
542
  | Common Causes | The node produces an output that is not needed by the current workflow. A connection from this port was removed but the port still exists. Control flow ports (`onSuccess`, `onFailure`) and scoped ports are excluded from this check. |
543
- | Fix | If the output is needed, connect it to a downstream node or to `Exit`. If not needed, the warning can be ignored, but it may indicate an incomplete workflow. |
543
+ | Fix | If the output is needed, connect it to a downstream node or to `Exit`. If the output is intentionally discarded, add `[suppress: "UNUSED_OUTPUT_PORT"]` to the `@node` annotation to silence this warning for that instance. |
544
544
 
545
545
  #### UNREACHABLE_EXIT_PORT (warning)
546
546
 
@@ -212,8 +212,8 @@ attributeBracket ::= "[" nodeAttr { "," nodeAttr } "]"
212
212
 
213
213
  nodeAttr ::= labelAttr | exprAttr | portOrderAttr | portLabelAttr
214
214
  | minimizedAttr | pullExecutionAttr | sizeAttr
215
- | colorAttr | iconAttr | tagsAttr | positionAttr
216
- | jobAttr | environmentAttr
215
+ | colorAttr | iconAttr | tagsAttr | suppressAttr
216
+ | positionAttr | jobAttr | environmentAttr
217
217
 
218
218
  labelAttr ::= "label:" STRING
219
219
  exprAttr ::= "expr:" IDENTIFIER "=" STRING { "," IDENTIFIER "=" STRING }
@@ -229,6 +229,7 @@ tagEntry ::= STRING [ STRING ]
229
229
  positionAttr ::= "position:" INTEGER INTEGER
230
230
  jobAttr ::= "job:" STRING
231
231
  environmentAttr ::= "environment:" STRING
232
+ suppressAttr ::= "suppress:" STRING { "," STRING }
232
233
  ```
233
234
 
234
235
  Multiple attribute brackets are allowed (zero or more). Attributes can be split across brackets or combined in one.
@@ -250,6 +251,7 @@ Multiple attribute brackets are allowed (zero or more). Attributes can be split
250
251
  @node myAdd Add [label: "hi"] [color: "#f00"] [position: 360 0]
251
252
  @node build npmBuild [job: "build"]
252
253
  @node deploy deploySsh [job: "deploy"] [environment: "production"]
254
+ @node fetch fetchData [suppress: "UNUSED_OUTPUT_PORT"]
253
255
  ```
254
256
 
255
257
  ## @connect