@synergenius/flow-weaver 0.6.0 → 0.8.0

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 (46) hide show
  1. package/dist/annotation-generator.js +11 -9
  2. package/dist/api/generate-in-place.js +28 -20
  3. package/dist/api/patterns.js +12 -15
  4. package/dist/ast/types.d.ts +2 -0
  5. package/dist/built-in-nodes/invoke-workflow.js +3 -3
  6. package/dist/built-in-nodes/mock-types.d.ts +20 -0
  7. package/dist/built-in-nodes/mock-types.js +30 -0
  8. package/dist/built-in-nodes/wait-for-agent.js +5 -4
  9. package/dist/built-in-nodes/wait-for-event.js +3 -3
  10. package/dist/chevrotain-parser/node-parser.d.ts +4 -0
  11. package/dist/chevrotain-parser/node-parser.js +24 -1
  12. package/dist/chevrotain-parser/tokens.d.ts +1 -0
  13. package/dist/chevrotain-parser/tokens.js +5 -0
  14. package/dist/cli/commands/compile.js +6 -0
  15. package/dist/cli/commands/init.js +2 -0
  16. package/dist/cli/commands/run.d.ts +2 -0
  17. package/dist/cli/commands/run.js +38 -0
  18. package/dist/cli/commands/validate.js +12 -0
  19. package/dist/cli/flow-weaver.mjs +208 -122
  20. package/dist/cli/templates/workflows/aggregator.js +3 -6
  21. package/dist/cli/templates/workflows/ai-agent-durable.js +4 -8
  22. package/dist/cli/templates/workflows/ai-agent.js +3 -6
  23. package/dist/cli/templates/workflows/ai-chat.js +3 -6
  24. package/dist/cli/templates/workflows/ai-pipeline-durable.js +4 -8
  25. package/dist/cli/templates/workflows/ai-rag.js +2 -4
  26. package/dist/cli/templates/workflows/ai-react.js +3 -6
  27. package/dist/cli/templates/workflows/conditional.js +3 -6
  28. package/dist/cli/templates/workflows/error-handler.js +2 -4
  29. package/dist/cli/templates/workflows/foreach.js +3 -6
  30. package/dist/cli/templates/workflows/sequential.js +7 -8
  31. package/dist/cli/templates/workflows/webhook.js +3 -6
  32. package/dist/doc-metadata/extractors/annotations.js +9 -6
  33. package/dist/editor-completions/jsDocAnnotations.js +5 -3
  34. package/dist/jsdoc-parser.d.ts +2 -0
  35. package/dist/jsdoc-parser.js +6 -1
  36. package/dist/mcp/tools-editor.js +20 -1
  37. package/dist/mcp/workflow-executor.js +7 -2
  38. package/dist/parser.js +1 -0
  39. package/dist/validator.js +19 -0
  40. package/docs/reference/concepts.md +52 -11
  41. package/docs/reference/debugging.md +33 -0
  42. package/docs/reference/iterative-development.md +2 -3
  43. package/docs/reference/jsdoc-grammar.md +8 -3
  44. package/docs/reference/patterns.md +2 -4
  45. package/docs/reference/tutorial.md +8 -14
  46. package/package.json +1 -1
@@ -40,9 +40,11 @@ export class AnnotationGenerator {
40
40
  }
41
41
  // Generate JSDoc comment block (only when no functionText)
42
42
  lines.push("/**");
43
- // Add description if present
43
+ // Add description if present (handle multi-line descriptions)
44
44
  if (includeComments && nodeType.description) {
45
- lines.push(` * ${nodeType.description}`);
45
+ for (const descLine of nodeType.description.split('\n')) {
46
+ lines.push(` * ${descLine}`);
47
+ }
46
48
  lines.push(` *`);
47
49
  }
48
50
  // @flowWeaver nodeType marker
@@ -279,12 +281,7 @@ export class AnnotationGenerator {
279
281
  if (workflow.ui?.startNode?.x !== undefined && workflow.ui?.startNode?.y !== undefined) {
280
282
  lines.push(` * @position Start ${Math.round(workflow.ui.startNode.x)} ${Math.round(workflow.ui.startNode.y)}`);
281
283
  }
282
- // Add instance positions
283
- workflow.instances.forEach((instance) => {
284
- if (instance.config?.x !== undefined && instance.config?.y !== undefined) {
285
- lines.push(` * @position ${instance.id} ${Math.round(instance.config.x)} ${Math.round(instance.config.y)}`);
286
- }
287
- });
284
+ // Instance positions are now emitted as [position: x y] on @node lines
288
285
  // Add Exit node position if present
289
286
  if (workflow.ui?.exitNode?.x !== undefined && workflow.ui?.exitNode?.y !== undefined) {
290
287
  lines.push(` * @position Exit ${Math.round(workflow.ui.exitNode.x)} ${Math.round(workflow.ui.exitNode.y)}`);
@@ -610,7 +607,12 @@ export function generateNodeInstanceTag(instance) {
610
607
  if (instance.config?.width !== undefined && instance.config?.height !== undefined) {
611
608
  sizeAttr = ` [size: ${Math.round(instance.config.width)} ${Math.round(instance.config.height)}]`;
612
609
  }
613
- return ` * @node ${instance.id} ${instance.nodeType}${parent}${labelAttr}${portOrderAttr}${portLabelAttr}${exprAttr}${pullExecutionAttr}${minimizedAttr}${colorAttr}${iconAttr}${tagsAttr}${sizeAttr}`;
610
+ // Generate [position: x y] attribute if present
611
+ let positionAttr = '';
612
+ if (instance.config?.x !== undefined && instance.config?.y !== undefined) {
613
+ positionAttr = ` [position: ${Math.round(instance.config.x)} ${Math.round(instance.config.y)}]`;
614
+ }
615
+ return ` * @node ${instance.id} ${instance.nodeType}${parent}${labelAttr}${portOrderAttr}${portLabelAttr}${exprAttr}${pullExecutionAttr}${minimizedAttr}${colorAttr}${iconAttr}${tagsAttr}${sizeAttr}${positionAttr}`;
614
616
  }
615
617
  export const annotationGenerator = new AnnotationGenerator();
616
618
  //# sourceMappingURL=annotation-generator.js.map
@@ -408,6 +408,9 @@ function replaceWorkflowFunctionBody(source, functionName, newBody) {
408
408
  }
409
409
  // Find the closing brace
410
410
  const closeBraceIdx = functionNode.body.end - 1;
411
+ if (closeBraceIdx <= openBraceIdx) {
412
+ return { code: source, changed: false };
413
+ }
411
414
  const before = source.slice(0, openBraceIdx + 1);
412
415
  const after = source.slice(closeBraceIdx);
413
416
  const newBodyWithMarkers = [
@@ -1116,17 +1119,36 @@ function generateWorkflowJSDoc(ast, options = {}) {
1116
1119
  }
1117
1120
  lines.push(` * @fwImport ${npmType.name} ${actualFunctionName} from "${npmType.importSource}"`);
1118
1121
  }
1119
- // Add node instances skip synthetic MAP_ITERATOR instances, strip parent from macro children
1122
+ // Auto-position: compute default positions for nodes without explicit positions.
1123
+ // Must happen before instance tags are generated so [position:] can be emitted.
1124
+ const autoPositions = computeAutoPositions(ast);
1125
+ // Add node instances — skip synthetic MAP_ITERATOR instances, strip parent from macro children.
1126
+ // Merge auto-computed positions into instance config (without mutating the AST).
1120
1127
  for (const instance of ast.instances) {
1121
1128
  if (macroInstanceIds.has(instance.id))
1122
1129
  continue;
1123
- if (macroChildIds.has(instance.id) && instance.parent) {
1130
+ // Merge auto-position into config if not already set
1131
+ let inst = instance;
1132
+ if (inst.config?.x === undefined || inst.config?.y === undefined) {
1133
+ const autoPos = autoPositions.get(inst.id);
1134
+ if (autoPos) {
1135
+ inst = {
1136
+ ...inst,
1137
+ config: {
1138
+ ...inst.config,
1139
+ x: inst.config?.x ?? autoPos.x,
1140
+ y: inst.config?.y ?? autoPos.y,
1141
+ },
1142
+ };
1143
+ }
1144
+ }
1145
+ if (macroChildIds.has(inst.id) && inst.parent) {
1124
1146
  // Write child @node without parent scope — @map handles it
1125
- const stripped = { ...instance, parent: undefined };
1147
+ const stripped = { ...inst, parent: undefined };
1126
1148
  lines.push(generateNodeInstanceTag(stripped));
1127
1149
  }
1128
1150
  else {
1129
- lines.push(generateNodeInstanceTag(instance));
1151
+ lines.push(generateNodeInstanceTag(inst));
1130
1152
  }
1131
1153
  }
1132
1154
  // Filter stale macros (e.g. paths whose connections were deleted)
@@ -1165,27 +1187,13 @@ function generateWorkflowJSDoc(ast, options = {}) {
1165
1187
  }
1166
1188
  }
1167
1189
  }
1168
- // Auto-position: compute default positions for nodes without explicit positions.
1169
- // Uses a left-to-right layout with topological ordering when connections are available.
1170
- const autoPositions = computeAutoPositions(ast);
1171
- // Add positions - Start node
1190
+ // Add positions - Start node (virtual, standalone @position)
1172
1191
  const startX = ast.ui?.startNode?.x ?? autoPositions.get('Start')?.x;
1173
1192
  const startY = ast.ui?.startNode?.y ?? autoPositions.get('Start')?.y;
1174
1193
  if (startX !== undefined && startY !== undefined) {
1175
1194
  lines.push(` * @position Start ${Math.round(startX)} ${Math.round(startY)}`);
1176
1195
  }
1177
- // Add positions - instances (use explicit position if available, otherwise auto-computed)
1178
- for (const instance of ast.instances) {
1179
- const explicitX = instance.config?.x;
1180
- const explicitY = instance.config?.y;
1181
- const autoPos = autoPositions.get(instance.id);
1182
- const x = explicitX ?? autoPos?.x;
1183
- const y = explicitY ?? autoPos?.y;
1184
- if (x !== undefined && y !== undefined) {
1185
- lines.push(` * @position ${instance.id} ${Math.round(x)} ${Math.round(y)}`);
1186
- }
1187
- }
1188
- // Add positions - Exit node
1196
+ // Add positions - Exit node (virtual, standalone @position)
1189
1197
  const exitX = ast.ui?.exitNode?.x ?? autoPositions.get('Exit')?.x;
1190
1198
  const exitY = ast.ui?.exitNode?.y ?? autoPositions.get('Exit')?.y;
1191
1199
  if (exitX !== undefined && exitY !== undefined) {
@@ -48,8 +48,13 @@ export function applyPattern(options) {
48
48
  conflicts.push(nodeType.name);
49
49
  }
50
50
  }
51
- // ── Build @node declarations ────────────────────────────────────────
52
- const nodeDeclarations = pattern.instances.map((inst) => ` * @node ${nodePrefix}${inst.id} ${inst.nodeType}`);
51
+ // ── Build @node declarations (with inline [position:] when present) ──
52
+ const nodeDeclarations = pattern.instances.map((inst) => {
53
+ const posAttr = inst.config?.x !== undefined && inst.config?.y !== undefined
54
+ ? ` [position: ${inst.config.x} ${inst.config.y}]`
55
+ : '';
56
+ return ` * @node ${nodePrefix}${inst.id} ${inst.nodeType}${posAttr}`;
57
+ });
53
58
  // ── Build @connect declarations + wiring instructions ───────────────
54
59
  const connectDeclarations = [];
55
60
  const wiringInstructions = [];
@@ -85,10 +90,6 @@ export function applyPattern(options) {
85
90
  connectDeclarations.push(` * @connect ${fromNode}.${conn.from.port} -> ${toNode}.${conn.to.port}`);
86
91
  }
87
92
  }
88
- // ── Build @position declarations ────────────────────────────────────
89
- const positionDeclarations = pattern.instances
90
- .filter((inst) => inst.config?.x !== undefined && inst.config?.y !== undefined)
91
- .map((inst) => ` * @position ${nodePrefix}${inst.id} ${inst.config.x} ${inst.config.y}`);
92
93
  // ── Generate node type functions (only non-conflicting) ─────────────
93
94
  const nodeTypesAdded = [];
94
95
  const nodeTypeFunctions = [];
@@ -103,7 +104,6 @@ export function applyPattern(options) {
103
104
  `// --- Pattern: ${pattern.name} ${prefix ? `(prefix: ${prefix})` : ''} ---`,
104
105
  ...nodeDeclarations,
105
106
  ...connectDeclarations,
106
- ...positionDeclarations,
107
107
  ];
108
108
  // ── Insert into target content ──────────────────────────────────────
109
109
  const workflowMatch = targetContent.match(/\/\*\*[\s\S]*?@flowWeaver\s+workflow[\s\S]*?\*\//);
@@ -250,9 +250,12 @@ export function extractPattern(options) {
250
250
  lines.push('/**');
251
251
  lines.push(` * @flowWeaver pattern`);
252
252
  lines.push(` * @name ${patternName}`);
253
- // Node declarations
253
+ // Node declarations (with inline [position:] when present)
254
254
  for (const inst of extractedInstances) {
255
- lines.push(` * @node ${inst.id} ${inst.nodeType}`);
255
+ const posAttr = inst.config?.x !== undefined && inst.config?.y !== undefined
256
+ ? ` [position: ${inst.config.x} ${inst.config.y}]`
257
+ : '';
258
+ lines.push(` * @node ${inst.id} ${inst.nodeType}${posAttr}`);
256
259
  }
257
260
  // Internal connections
258
261
  for (const conn of internalConnections) {
@@ -280,12 +283,6 @@ export function extractPattern(options) {
280
283
  for (const port of [...new Set(outputPorts)]) {
281
284
  lines.push(` * @port OUT.${port}`);
282
285
  }
283
- // Positions
284
- for (const inst of extractedInstances) {
285
- if (inst.config?.x !== undefined && inst.config?.y !== undefined) {
286
- lines.push(` * @position ${inst.id} ${inst.config.x} ${inst.config.y}`);
287
- }
288
- }
289
286
  lines.push(' */');
290
287
  lines.push('function patternPlaceholder() {}');
291
288
  // Add node type functions
@@ -637,6 +637,8 @@ export type TValidationError = {
637
637
  node?: string;
638
638
  connection?: TConnectionAST;
639
639
  location?: TSourceLocation;
640
+ /** Reference to documentation explaining this error and how to fix it. */
641
+ docUrl?: string;
640
642
  };
641
643
  export type TAnalysisResult = {
642
644
  controlFlowGraph: TControlFlowGraph;
@@ -1,4 +1,4 @@
1
- import { getMockConfig } from './mock-types.js';
1
+ import { getMockConfig, lookupMock } from './mock-types.js';
2
2
  /**
3
3
  * @flowWeaver nodeType
4
4
  * @input functionId - Inngest function ID (e.g. "my-service/sub-workflow")
@@ -11,8 +11,8 @@ export async function invokeWorkflow(execute, functionId, payload, timeout) {
11
11
  return { onSuccess: false, onFailure: false, result: {} };
12
12
  const mocks = getMockConfig();
13
13
  if (mocks) {
14
- // Mock mode active — look up result by functionId
15
- const mockResult = mocks.invocations?.[functionId];
14
+ // Mock mode — look up result by functionId (supports instance-qualified keys)
15
+ const mockResult = lookupMock(mocks.invocations, functionId);
16
16
  if (mockResult !== undefined) {
17
17
  return { onSuccess: true, onFailure: false, result: mockResult };
18
18
  }
@@ -17,4 +17,24 @@ export interface FwMockConfig {
17
17
  * Read the mock config from globalThis, returning undefined if not set.
18
18
  */
19
19
  export declare function getMockConfig(): FwMockConfig | undefined;
20
+ /**
21
+ * Look up a mock value from a section, supporting instance-qualified keys.
22
+ *
23
+ * Checks "instanceId:key" first (for per-node targeting), then falls back
24
+ * to plain "key". The instance ID comes from __fw_current_node_id__ which
25
+ * the generated code sets before each node invocation.
26
+ *
27
+ * @example
28
+ * ```json
29
+ * {
30
+ * "invocations": {
31
+ * "retryCall:api/process": { "status": "ok" },
32
+ * "api/process": { "status": "default" }
33
+ * }
34
+ * }
35
+ * ```
36
+ * When the node "retryCall" invokes "api/process", it gets `{ status: "ok" }`.
37
+ * Any other node invoking "api/process" gets `{ status: "default" }`.
38
+ */
39
+ export declare function lookupMock<T>(section: Record<string, T> | undefined, key: string): T | undefined;
20
40
  //# sourceMappingURL=mock-types.d.ts.map
@@ -9,4 +9,34 @@
9
9
  export function getMockConfig() {
10
10
  return globalThis.__fw_mocks__;
11
11
  }
12
+ /**
13
+ * Look up a mock value from a section, supporting instance-qualified keys.
14
+ *
15
+ * Checks "instanceId:key" first (for per-node targeting), then falls back
16
+ * to plain "key". The instance ID comes from __fw_current_node_id__ which
17
+ * the generated code sets before each node invocation.
18
+ *
19
+ * @example
20
+ * ```json
21
+ * {
22
+ * "invocations": {
23
+ * "retryCall:api/process": { "status": "ok" },
24
+ * "api/process": { "status": "default" }
25
+ * }
26
+ * }
27
+ * ```
28
+ * When the node "retryCall" invokes "api/process", it gets `{ status: "ok" }`.
29
+ * Any other node invoking "api/process" gets `{ status: "default" }`.
30
+ */
31
+ export function lookupMock(section, key) {
32
+ if (!section)
33
+ return undefined;
34
+ const nodeId = globalThis.__fw_current_node_id__;
35
+ if (nodeId) {
36
+ const qualified = section[`${nodeId}:${key}`];
37
+ if (qualified !== undefined)
38
+ return qualified;
39
+ }
40
+ return section[key];
41
+ }
12
42
  //# sourceMappingURL=mock-types.js.map
@@ -1,4 +1,4 @@
1
- import { getMockConfig } from './mock-types.js';
1
+ import { getMockConfig, lookupMock } from './mock-types.js';
2
2
  /**
3
3
  * @flowWeaver nodeType
4
4
  * @input agentId - Agent/task identifier
@@ -9,10 +9,11 @@ import { getMockConfig } from './mock-types.js';
9
9
  export async function waitForAgent(execute, agentId, context, prompt) {
10
10
  if (!execute)
11
11
  return { onSuccess: false, onFailure: false, agentResult: {} };
12
- // 1. Check mocks first
12
+ // 1. Check mocks first (supports instance-qualified keys)
13
13
  const mocks = getMockConfig();
14
- if (mocks?.agents?.[agentId]) {
15
- return { onSuccess: true, onFailure: false, agentResult: mocks.agents[agentId] };
14
+ const mockResult = lookupMock(mocks?.agents, agentId);
15
+ if (mockResult !== undefined) {
16
+ return { onSuccess: true, onFailure: false, agentResult: mockResult };
16
17
  }
17
18
  // 2. Check agent channel (set by executor for pause/resume)
18
19
  const channel = globalThis.__fw_agent_channel__;
@@ -1,4 +1,4 @@
1
- import { getMockConfig } from './mock-types.js';
1
+ import { getMockConfig, lookupMock } from './mock-types.js';
2
2
  /**
3
3
  * @flowWeaver nodeType
4
4
  * @input eventName - Event name to wait for (e.g. "app/approval.received")
@@ -11,8 +11,8 @@ export async function waitForEvent(execute, eventName, match, timeout) {
11
11
  return { onSuccess: false, onFailure: false, eventData: {} };
12
12
  const mocks = getMockConfig();
13
13
  if (mocks) {
14
- // Mock mode active — look up event data by name
15
- const mockData = mocks.events?.[eventName];
14
+ // Mock mode — look up event data by name (supports instance-qualified keys)
15
+ const mockData = lookupMock(mocks.events, eventName);
16
16
  if (mockData !== undefined) {
17
17
  return { onSuccess: true, onFailure: false, eventData: mockData };
18
18
  }
@@ -17,6 +17,10 @@ export interface NodeParseResult {
17
17
  width: number;
18
18
  height: number;
19
19
  };
20
+ position?: {
21
+ x: number;
22
+ y: number;
23
+ };
20
24
  color?: string;
21
25
  icon?: string;
22
26
  tags?: Array<{
@@ -4,7 +4,7 @@
4
4
  * Parser for @node declarations using Chevrotain.
5
5
  */
6
6
  import { CstParser } from 'chevrotain';
7
- import { JSDocLexer, NodeTag, Identifier, Dot, Integer, LabelPrefix, ExprPrefix, PortOrderPrefix, PortLabelPrefix, MinimizedKeyword, PullExecutionPrefix, SizePrefix, ColorPrefix, IconPrefix, TagsPrefix, StringLiteral, LBracket, RBracket, Comma, Equals, EventEq, CronEq, MatchEq, TimeoutEq, LimitEq, PeriodEq, allTokens, } from './tokens.js';
7
+ import { JSDocLexer, NodeTag, Identifier, Dot, Integer, LabelPrefix, ExprPrefix, PortOrderPrefix, PortLabelPrefix, MinimizedKeyword, PullExecutionPrefix, SizePrefix, PositionPrefix, ColorPrefix, IconPrefix, TagsPrefix, StringLiteral, LBracket, RBracket, Comma, Equals, EventEq, CronEq, MatchEq, TimeoutEq, LimitEq, PeriodEq, allTokens, } from './tokens.js';
8
8
  // =============================================================================
9
9
  // Parser Definition
10
10
  // =============================================================================
@@ -48,6 +48,7 @@ class NodeParser extends CstParser {
48
48
  { ALT: () => this.SUBRULE(this.minimizedAttr) },
49
49
  { ALT: () => this.SUBRULE(this.pullExecutionAttr) },
50
50
  { ALT: () => this.SUBRULE(this.sizeAttr) },
51
+ { ALT: () => this.SUBRULE(this.positionAttr) },
51
52
  { ALT: () => this.SUBRULE(this.colorAttr) },
52
53
  { ALT: () => this.SUBRULE(this.iconAttr) },
53
54
  { ALT: () => this.SUBRULE(this.tagsAttr) },
@@ -140,6 +141,12 @@ class NodeParser extends CstParser {
140
141
  this.CONSUME(Integer, { LABEL: 'widthValue' });
141
142
  this.CONSUME2(Integer, { LABEL: 'heightValue' });
142
143
  });
144
+ // position: x y
145
+ positionAttr = this.RULE('positionAttr', () => {
146
+ this.CONSUME(PositionPrefix);
147
+ this.CONSUME(Integer, { LABEL: 'xValue' });
148
+ this.CONSUME2(Integer, { LABEL: 'yValue' });
149
+ });
143
150
  // color: "value"
144
151
  colorAttr = this.RULE('colorAttr', () => {
145
152
  this.CONSUME(ColorPrefix);
@@ -192,6 +199,7 @@ class NodeVisitor extends BaseVisitor {
192
199
  let minimized;
193
200
  let pullExecution;
194
201
  let size;
202
+ let position;
195
203
  let color;
196
204
  let icon;
197
205
  let tags;
@@ -215,6 +223,8 @@ class NodeVisitor extends BaseVisitor {
215
223
  pullExecution = attrs.pullExecution;
216
224
  if (attrs.size)
217
225
  size = attrs.size;
226
+ if (attrs.position)
227
+ position = attrs.position;
218
228
  if (attrs.color)
219
229
  color = attrs.color;
220
230
  if (attrs.icon)
@@ -234,6 +244,7 @@ class NodeVisitor extends BaseVisitor {
234
244
  ...(minimized && { minimized }),
235
245
  ...(pullExecution && { pullExecution }),
236
246
  ...(size && { size }),
247
+ ...(position && { position }),
237
248
  ...(color && { color }),
238
249
  ...(icon && { icon }),
239
250
  ...(tags && { tags }),
@@ -252,6 +263,7 @@ class NodeVisitor extends BaseVisitor {
252
263
  let minimized;
253
264
  let pullExecution;
254
265
  let size;
266
+ let position;
255
267
  let color;
256
268
  let icon;
257
269
  let tags;
@@ -291,6 +303,11 @@ class NodeVisitor extends BaseVisitor {
291
303
  size = this.visit(attr);
292
304
  }
293
305
  }
306
+ if (ctx.positionAttr) {
307
+ for (const attr of ctx.positionAttr) {
308
+ position = this.visit(attr);
309
+ }
310
+ }
294
311
  if (ctx.colorAttr) {
295
312
  for (const attr of ctx.colorAttr) {
296
313
  color = this.visit(attr);
@@ -315,6 +332,7 @@ class NodeVisitor extends BaseVisitor {
315
332
  minimized,
316
333
  pullExecution,
317
334
  size,
335
+ position,
318
336
  color,
319
337
  icon,
320
338
  tags,
@@ -397,6 +415,11 @@ class NodeVisitor extends BaseVisitor {
397
415
  const height = parseInt(ctx.heightValue[0].image, 10);
398
416
  return { width, height };
399
417
  }
418
+ positionAttr(ctx) {
419
+ const x = parseInt(ctx.xValue[0].image, 10);
420
+ const y = parseInt(ctx.yValue[0].image, 10);
421
+ return { x, y };
422
+ }
400
423
  colorAttr(ctx) {
401
424
  return this.unescapeString(ctx.colorValue[0].image);
402
425
  }
@@ -44,6 +44,7 @@ export declare const MergeStrategyPrefix: import("chevrotain").TokenType;
44
44
  export declare const PullExecutionPrefix: import("chevrotain").TokenType;
45
45
  export declare const MinimizedKeyword: import("chevrotain").TokenType;
46
46
  export declare const SizePrefix: import("chevrotain").TokenType;
47
+ export declare const PositionPrefix: import("chevrotain").TokenType;
47
48
  export declare const ColorPrefix: import("chevrotain").TokenType;
48
49
  export declare const IconPrefix: import("chevrotain").TokenType;
49
50
  export declare const TagsPrefix: import("chevrotain").TokenType;
@@ -172,6 +172,10 @@ export const SizePrefix = createToken({
172
172
  name: 'SizePrefix',
173
173
  pattern: /size:/,
174
174
  });
175
+ export const PositionPrefix = createToken({
176
+ name: 'PositionPrefix',
177
+ pattern: /position:/,
178
+ });
175
179
  export const ColorPrefix = createToken({
176
180
  name: 'ColorPrefix',
177
181
  pattern: /color:/,
@@ -356,6 +360,7 @@ export const allTokens = [
356
360
  MergeStrategyPrefix,
357
361
  PullExecutionPrefix,
358
362
  SizePrefix,
363
+ PositionPrefix,
359
364
  ColorPrefix,
360
365
  IconPrefix,
361
366
  TagsPrefix,
@@ -116,6 +116,9 @@ export async function compileCommand(input, options = {}) {
116
116
  const loc = err.location ? `[line ${err.location.line}] ` : '';
117
117
  logger.error(` ${loc}${friendly.title}: ${friendly.explanation}`);
118
118
  logger.info(` How to fix: ${friendly.fix}`);
119
+ if (err.docUrl) {
120
+ logger.info(` See: ${err.docUrl}`);
121
+ }
119
122
  }
120
123
  else {
121
124
  let msg = ` - ${err.message}`;
@@ -123,6 +126,9 @@ export async function compileCommand(input, options = {}) {
123
126
  msg += ` (node: ${err.node})`;
124
127
  }
125
128
  logger.error(msg);
129
+ if (err.docUrl) {
130
+ logger.info(` See: ${err.docUrl}`);
131
+ }
126
132
  }
127
133
  });
128
134
  errorCount++;
@@ -273,12 +273,14 @@ export function generateProjectFiles(projectName, template, format = 'esm') {
273
273
  ].join('\n');
274
274
  }
275
275
  const gitignore = `node_modules/\ndist/\n.tsbuildinfo\n`;
276
+ const configYaml = `defaultFileType: ts\n`;
276
277
  return {
277
278
  'package.json': packageJson,
278
279
  'tsconfig.json': tsconfigJson,
279
280
  [`src/${workflowFile}`]: workflowCode,
280
281
  'src/main.ts': mainTs,
281
282
  '.gitignore': gitignore,
283
+ '.flowweaver/config.yaml': configYaml,
282
284
  };
283
285
  }
284
286
  // ── Filesystem writer ────────────────────────────────────────────────────────
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Run command - execute a workflow file directly from the CLI
3
3
  */
4
+ import type { FwMockConfig } from '../../built-in-nodes/mock-types.js';
4
5
  export interface RunOptions {
5
6
  /** Specific workflow name to run (if file contains multiple workflows) */
6
7
  workflow?: string;
@@ -48,4 +49,5 @@ export interface RunOptions {
48
49
  * ```
49
50
  */
50
51
  export declare function runCommand(input: string, options: RunOptions): Promise<void>;
52
+ export declare function validateMockConfig(mocks: FwMockConfig, filePath: string, workflowName?: string): Promise<void>;
51
53
  //# sourceMappingURL=run.d.ts.map
@@ -9,6 +9,7 @@ import { AgentChannel } from '../../mcp/agent-channel.js';
9
9
  import { logger } from '../utils/logger.js';
10
10
  import { getFriendlyError } from '../../friendly-errors.js';
11
11
  import { getErrorMessage } from '../../utils/error-utils.js';
12
+ import { parseWorkflow } from '../../api/index.js';
12
13
  /**
13
14
  * Execute a workflow file and output the result.
14
15
  *
@@ -85,6 +86,10 @@ export async function runCommand(input, options) {
85
86
  throw new Error(`Failed to parse mocks file: ${options.mocksFile}`);
86
87
  }
87
88
  }
89
+ // Validate mock config against workflow when mocks are provided
90
+ if (mocks && !options.json) {
91
+ await validateMockConfig(mocks, filePath, options.workflow);
92
+ }
88
93
  // Set up timeout if specified
89
94
  let timeoutId;
90
95
  let timedOut = false;
@@ -263,6 +268,39 @@ export async function runCommand(input, options) {
263
268
  }
264
269
  }
265
270
  }
271
+ const VALID_MOCK_KEYS = new Set(['events', 'invocations', 'agents', 'fast']);
272
+ const BUILT_IN_NODE_TYPES = new Set(['delay', 'waitForEvent', 'invokeWorkflow', 'waitForAgent']);
273
+ const MOCK_SECTION_TO_NODE = {
274
+ events: 'waitForEvent',
275
+ invocations: 'invokeWorkflow',
276
+ agents: 'waitForAgent',
277
+ };
278
+ export async function validateMockConfig(mocks, filePath, workflowName) {
279
+ // Check for unknown top-level keys (catches typos like "invocation" instead of "invocations")
280
+ for (const key of Object.keys(mocks)) {
281
+ if (!VALID_MOCK_KEYS.has(key)) {
282
+ logger.warn(`Mock config has unknown key "${key}". Valid keys: ${[...VALID_MOCK_KEYS].join(', ')}`);
283
+ }
284
+ }
285
+ // Quick-parse the workflow to check which built-in node types are used
286
+ try {
287
+ const result = await parseWorkflow(filePath, { workflowName });
288
+ if (result.errors.length > 0 || !result.ast?.instances)
289
+ return;
290
+ const usedNodeTypes = new Set(result.ast.instances.map((i) => i.nodeType));
291
+ for (const [section, nodeType] of Object.entries(MOCK_SECTION_TO_NODE)) {
292
+ const mockSection = mocks[section];
293
+ if (mockSection && typeof mockSection === 'object' && Object.keys(mockSection).length > 0) {
294
+ if (!usedNodeTypes.has(nodeType)) {
295
+ logger.warn(`Mock config has "${section}" entries but workflow has no ${nodeType} nodes`);
296
+ }
297
+ }
298
+ }
299
+ }
300
+ catch {
301
+ // Parsing failed — skip validation, the execution will report the real error
302
+ }
303
+ }
266
304
  function promptForInput(question) {
267
305
  return new Promise((resolve) => {
268
306
  const rl = readline.createInterface({
@@ -140,6 +140,9 @@ export async function validateCommand(input, options = {}) {
140
140
  const loc = err.location ? `[line ${err.location.line}] ` : '';
141
141
  logger.error(` ${loc}${friendly.title}: ${friendly.explanation}`);
142
142
  logger.info(` How to fix: ${friendly.fix}`);
143
+ if (err.docUrl) {
144
+ logger.info(` See: ${err.docUrl}`);
145
+ }
143
146
  }
144
147
  else {
145
148
  let msg = ` - ${err.message}`;
@@ -153,6 +156,9 @@ export async function validateCommand(input, options = {}) {
153
156
  msg += ` (connection: ${err.connection.from.node}:${err.connection.from.port} -> ${err.connection.to.node}:${err.connection.to.port})`;
154
157
  }
155
158
  logger.error(msg);
159
+ if (err.docUrl) {
160
+ logger.info(` See: ${err.docUrl}`);
161
+ }
156
162
  }
157
163
  });
158
164
  }
@@ -167,6 +173,9 @@ export async function validateCommand(input, options = {}) {
167
173
  const loc = warn.location ? `[line ${warn.location.line}] ` : '';
168
174
  logger.warn(` ${loc}${friendly.title}: ${friendly.explanation}`);
169
175
  logger.info(` How to fix: ${friendly.fix}`);
176
+ if (warn.docUrl) {
177
+ logger.info(` See: ${warn.docUrl}`);
178
+ }
170
179
  }
171
180
  else {
172
181
  let msg = ` - ${warn.message}`;
@@ -177,6 +186,9 @@ export async function validateCommand(input, options = {}) {
177
186
  msg += ` (node: ${warn.node})`;
178
187
  }
179
188
  logger.warn(msg);
189
+ if (warn.docUrl) {
190
+ logger.info(` See: ${warn.docUrl}`);
191
+ }
180
192
  }
181
193
  });
182
194
  }