@synergenius/flow-weaver 0.6.0 → 0.7.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.
@@ -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
@@ -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 = [
@@ -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
  }
@@ -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
  }
@@ -11870,7 +11870,7 @@ var init_type_checker = __esm({
11870
11870
  });
11871
11871
 
11872
11872
  // src/validator.ts
11873
- var WorkflowValidator, validator;
11873
+ var DOCS_BASE, ERROR_DOC_URLS, WorkflowValidator, validator;
11874
11874
  var init_validator = __esm({
11875
11875
  "src/validator.ts"() {
11876
11876
  "use strict";
@@ -11878,6 +11878,18 @@ var init_validator = __esm({
11878
11878
  init_string_distance();
11879
11879
  init_signature_parser();
11880
11880
  init_type_checker();
11881
+ DOCS_BASE = "https://docs.flowweaver.dev/reference";
11882
+ ERROR_DOC_URLS = {
11883
+ UNKNOWN_NODE_TYPE: `${DOCS_BASE}/concepts#node-registration`,
11884
+ UNKNOWN_SOURCE_PORT: `${DOCS_BASE}/concepts#port-architecture`,
11885
+ UNKNOWN_TARGET_PORT: `${DOCS_BASE}/concepts#port-architecture`,
11886
+ TYPE_MISMATCH: `${DOCS_BASE}/compilation#type-compatibility`,
11887
+ UNREACHABLE_NODE: `${DOCS_BASE}/concepts#graph-structure`,
11888
+ MISSING_START_CONNECTION: `${DOCS_BASE}/concepts#start-and-exit`,
11889
+ MISSING_EXIT_CONNECTION: `${DOCS_BASE}/concepts#start-and-exit`,
11890
+ INFERRED_NODE_TYPE: `${DOCS_BASE}/node-conversion`,
11891
+ DUPLICATE_CONNECTION: `${DOCS_BASE}/concepts#connections`
11892
+ };
11881
11893
  WorkflowValidator = class {
11882
11894
  errors = [];
11883
11895
  warnings = [];
@@ -12005,6 +12017,11 @@ var init_validator = __esm({
12005
12017
  return true;
12006
12018
  });
12007
12019
  }
12020
+ for (const diag of [...this.errors, ...this.warnings]) {
12021
+ if (!diag.docUrl && ERROR_DOC_URLS[diag.code]) {
12022
+ diag.docUrl = ERROR_DOC_URLS[diag.code];
12023
+ }
12024
+ }
12008
12025
  return {
12009
12026
  valid: this.errors.length === 0,
12010
12027
  errors: this.errors,
@@ -30777,7 +30794,9 @@ var AnnotationGenerator = class {
30777
30794
  }
30778
30795
  lines.push("/**");
30779
30796
  if (includeComments && nodeType.description) {
30780
- lines.push(` * ${nodeType.description}`);
30797
+ for (const descLine of nodeType.description.split("\n")) {
30798
+ lines.push(` * ${descLine}`);
30799
+ }
30781
30800
  lines.push(` *`);
30782
30801
  }
30783
30802
  lines.push(" * @flowWeaver nodeType");
@@ -31510,6 +31529,9 @@ function replaceWorkflowFunctionBody(source, functionName, newBody) {
31510
31529
  return { code: source, changed: false };
31511
31530
  }
31512
31531
  const closeBraceIdx = functionNode.body.end - 1;
31532
+ if (closeBraceIdx <= openBraceIdx) {
31533
+ return { code: source, changed: false };
31534
+ }
31513
31535
  const before = source.slice(0, openBraceIdx + 1);
31514
31536
  const after = source.slice(closeBraceIdx);
31515
31537
  const newBodyWithMarkers = [
@@ -57939,12 +57961,18 @@ async function compileCommand(input, options = {}) {
57939
57961
  const loc = err.location ? `[line ${err.location.line}] ` : "";
57940
57962
  logger.error(` ${loc}${friendly.title}: ${friendly.explanation}`);
57941
57963
  logger.info(` How to fix: ${friendly.fix}`);
57964
+ if (err.docUrl) {
57965
+ logger.info(` See: ${err.docUrl}`);
57966
+ }
57942
57967
  } else {
57943
57968
  let msg = ` - ${err.message}`;
57944
57969
  if (err.node) {
57945
57970
  msg += ` (node: ${err.node})`;
57946
57971
  }
57947
57972
  logger.error(msg);
57973
+ if (err.docUrl) {
57974
+ logger.info(` See: ${err.docUrl}`);
57975
+ }
57948
57976
  }
57949
57977
  });
57950
57978
  errorCount++;
@@ -62030,6 +62058,9 @@ async function validateCommand(input, options = {}) {
62030
62058
  const loc = err.location ? `[line ${err.location.line}] ` : "";
62031
62059
  logger.error(` ${loc}${friendly.title}: ${friendly.explanation}`);
62032
62060
  logger.info(` How to fix: ${friendly.fix}`);
62061
+ if (err.docUrl) {
62062
+ logger.info(` See: ${err.docUrl}`);
62063
+ }
62033
62064
  } else {
62034
62065
  let msg = ` - ${err.message}`;
62035
62066
  if (err.location) {
@@ -62042,6 +62073,9 @@ async function validateCommand(input, options = {}) {
62042
62073
  msg += ` (connection: ${err.connection.from.node}:${err.connection.from.port} -> ${err.connection.to.node}:${err.connection.to.port})`;
62043
62074
  }
62044
62075
  logger.error(msg);
62076
+ if (err.docUrl) {
62077
+ logger.info(` See: ${err.docUrl}`);
62078
+ }
62045
62079
  }
62046
62080
  });
62047
62081
  }
@@ -62056,6 +62090,9 @@ async function validateCommand(input, options = {}) {
62056
62090
  const loc = warn.location ? `[line ${warn.location.line}] ` : "";
62057
62091
  logger.warn(` ${loc}${friendly.title}: ${friendly.explanation}`);
62058
62092
  logger.info(` How to fix: ${friendly.fix}`);
62093
+ if (warn.docUrl) {
62094
+ logger.info(` See: ${warn.docUrl}`);
62095
+ }
62059
62096
  } else {
62060
62097
  let msg = ` - ${warn.message}`;
62061
62098
  if (warn.location) {
@@ -62065,6 +62102,9 @@ async function validateCommand(input, options = {}) {
62065
62102
  msg += ` (node: ${warn.node})`;
62066
62103
  }
62067
62104
  logger.warn(msg);
62105
+ if (warn.docUrl) {
62106
+ logger.info(` See: ${warn.docUrl}`);
62107
+ }
62068
62108
  }
62069
62109
  });
62070
62110
  }
@@ -65370,13 +65410,16 @@ function generateProjectFiles(projectName, template, format = "esm") {
65370
65410
  const gitignore = `node_modules/
65371
65411
  dist/
65372
65412
  .tsbuildinfo
65413
+ `;
65414
+ const configYaml = `defaultFileType: ts
65373
65415
  `;
65374
65416
  return {
65375
65417
  "package.json": packageJson,
65376
65418
  "tsconfig.json": tsconfigJson,
65377
65419
  [`src/${workflowFile}`]: workflowCode,
65378
65420
  "src/main.ts": mainTs,
65379
- ".gitignore": gitignore
65421
+ ".gitignore": gitignore,
65422
+ ".flowweaver/config.yaml": configYaml
65380
65423
  };
65381
65424
  }
65382
65425
  function scaffoldProject(targetDir, files, options) {
@@ -65576,22 +65619,19 @@ async function watchCommand(input, options = {}) {
65576
65619
  init_esm5();
65577
65620
  import * as path19 from "path";
65578
65621
  import * as fs18 from "fs";
65579
- import * as os2 from "os";
65622
+ import * as os from "os";
65580
65623
  import { spawn } from "child_process";
65581
65624
 
65582
65625
  // src/mcp/workflow-executor.ts
65583
65626
  import * as path18 from "path";
65584
65627
  import * as fs17 from "fs";
65585
- import * as os from "os";
65586
65628
  import { pathToFileURL } from "url";
65587
65629
  import ts4 from "typescript";
65588
65630
  async function executeWorkflowFromFile(filePath, params, options) {
65589
65631
  const resolvedPath = path18.resolve(filePath);
65590
65632
  const includeTrace = options?.includeTrace !== false;
65591
- const tmpBase = path18.join(
65592
- os.tmpdir(),
65593
- `fw-exec-${Date.now()}-${Math.random().toString(36).slice(2)}`
65594
- );
65633
+ const tmpId = `fw-exec-${Date.now()}-${Math.random().toString(36).slice(2)}`;
65634
+ const tmpBase = path18.join(path18.dirname(resolvedPath), tmpId);
65595
65635
  const tmpTsFile = `${tmpBase}.ts`;
65596
65636
  const tmpFile = `${tmpBase}.mjs`;
65597
65637
  try {
@@ -65887,7 +65927,7 @@ async function runInngestDevMode(filePath, options) {
65887
65927
  if (missingDeps.length > 0) {
65888
65928
  throw new Error(`Missing dependencies: ${missingDeps.join(", ")}. Install them with: npm install ${missingDeps.join(" ")}`);
65889
65929
  }
65890
- const tmpDir = fs18.mkdtempSync(path19.join(os2.tmpdir(), "fw-inngest-dev-"));
65930
+ const tmpDir = fs18.mkdtempSync(path19.join(os.tmpdir(), "fw-inngest-dev-"));
65891
65931
  const inngestOutputPath = path19.join(tmpDir, path19.basename(filePath).replace(/\.ts$/, ".inngest.ts"));
65892
65932
  let serverProcess = null;
65893
65933
  const compileInngest = async () => {
@@ -84262,15 +84302,30 @@ function registerEditorTools(mcp, connection, buffer) {
84262
84302
  params: external_exports.record(external_exports.unknown()).optional().describe("Optional execution parameters"),
84263
84303
  includeTrace: external_exports.boolean().optional().describe("Include execution trace events (default: true)")
84264
84304
  },
84265
- async (args) => {
84305
+ async (args, extra) => {
84266
84306
  if (args.filePath) {
84267
84307
  try {
84268
84308
  const channel = new AgentChannel();
84269
84309
  const runId = `run-${Date.now()}-${Math.random().toString(36).slice(2)}`;
84310
+ const progressToken = extra._meta?.progressToken;
84311
+ let eventCount = 0;
84312
+ const onEvent = progressToken ? (event) => {
84313
+ eventCount++;
84314
+ extra.sendNotification({
84315
+ method: "notifications/progress",
84316
+ params: {
84317
+ progressToken,
84318
+ progress: eventCount,
84319
+ message: event.type === "STATUS_CHANGED" ? `${event.data?.id ?? ""}: ${event.data?.status ?? ""}` : event.type
84320
+ }
84321
+ }).catch(() => {
84322
+ });
84323
+ } : void 0;
84270
84324
  const execPromise = executeWorkflowFromFile(args.filePath, args.params, {
84271
84325
  workflowName: args.workflowName,
84272
84326
  includeTrace: args.includeTrace,
84273
- agentChannel: channel
84327
+ agentChannel: channel,
84328
+ onEvent
84274
84329
  });
84275
84330
  const raceResult = await Promise.race([
84276
84331
  execPromise.then((r) => ({ type: "completed", result: r })),
@@ -92226,6 +92281,9 @@ async function runCommand(input, options) {
92226
92281
  throw new Error(`Failed to parse mocks file: ${options.mocksFile}`);
92227
92282
  }
92228
92283
  }
92284
+ if (mocks && !options.json) {
92285
+ await validateMockConfig(mocks, filePath, options.workflow);
92286
+ }
92229
92287
  let timeoutId;
92230
92288
  let timedOut = false;
92231
92289
  if (options.timeout) {
@@ -92380,6 +92438,35 @@ async function runCommand(input, options) {
92380
92438
  }
92381
92439
  }
92382
92440
  }
92441
+ var VALID_MOCK_KEYS = /* @__PURE__ */ new Set(["events", "invocations", "agents", "fast"]);
92442
+ var MOCK_SECTION_TO_NODE = {
92443
+ events: "waitForEvent",
92444
+ invocations: "invokeWorkflow",
92445
+ agents: "waitForAgent"
92446
+ };
92447
+ async function validateMockConfig(mocks, filePath, workflowName) {
92448
+ for (const key of Object.keys(mocks)) {
92449
+ if (!VALID_MOCK_KEYS.has(key)) {
92450
+ logger.warn(`Mock config has unknown key "${key}". Valid keys: ${[...VALID_MOCK_KEYS].join(", ")}`);
92451
+ }
92452
+ }
92453
+ try {
92454
+ const result = await parseWorkflow(filePath, { workflowName });
92455
+ if (result.errors.length > 0 || !result.ast?.instances) return;
92456
+ const usedNodeTypes = new Set(result.ast.instances.map((i) => i.nodeType));
92457
+ for (const [section, nodeType] of Object.entries(MOCK_SECTION_TO_NODE)) {
92458
+ const mockSection = mocks[section];
92459
+ if (mockSection && typeof mockSection === "object" && Object.keys(mockSection).length > 0) {
92460
+ if (!usedNodeTypes.has(nodeType)) {
92461
+ logger.warn(
92462
+ `Mock config has "${section}" entries but workflow has no ${nodeType} nodes`
92463
+ );
92464
+ }
92465
+ }
92466
+ }
92467
+ } catch {
92468
+ }
92469
+ }
92383
92470
  function promptForInput(question) {
92384
92471
  return new Promise((resolve27) => {
92385
92472
  const rl = readline9.createInterface({
@@ -92876,7 +92963,7 @@ async function serveCommand(dir, options) {
92876
92963
  // src/export/index.ts
92877
92964
  import * as path33 from "path";
92878
92965
  import * as fs32 from "fs";
92879
- import * as os3 from "os";
92966
+ import * as os2 from "os";
92880
92967
  import { fileURLToPath as fileURLToPath5 } from "url";
92881
92968
 
92882
92969
  // src/export/templates.ts
@@ -93073,7 +93160,7 @@ async function exportMultiWorkflow(options) {
93073
93160
  throw new Error(`None of the requested workflows found. Available: ${available}`);
93074
93161
  }
93075
93162
  }
93076
- const workDir = isDryRun ? path33.join(os3.tmpdir(), `fw-export-multi-dryrun-${Date.now()}`) : outputDir;
93163
+ const workDir = isDryRun ? path33.join(os2.tmpdir(), `fw-export-multi-dryrun-${Date.now()}`) : outputDir;
93077
93164
  fs32.mkdirSync(workDir, { recursive: true });
93078
93165
  fs32.mkdirSync(path33.join(workDir, "workflows"), { recursive: true });
93079
93166
  fs32.mkdirSync(path33.join(workDir, "runtime"), { recursive: true });
@@ -93797,7 +93884,7 @@ async function exportWorkflow(options) {
93797
93884
  const available = parseResult.workflows.map((w) => w.name).join(", ");
93798
93885
  throw new Error(`Workflow "${options.workflow}" not found. Available: ${available}`);
93799
93886
  }
93800
- const workDir = isDryRun ? path33.join(os3.tmpdir(), `fw-export-dryrun-${Date.now()}`) : outputDir;
93887
+ const workDir = isDryRun ? path33.join(os2.tmpdir(), `fw-export-dryrun-${Date.now()}`) : outputDir;
93801
93888
  fs32.mkdirSync(workDir, { recursive: true });
93802
93889
  let compiledContent;
93803
93890
  let compiledPath;
@@ -95041,7 +95128,7 @@ function displayInstalledPackage(pkg) {
95041
95128
  }
95042
95129
 
95043
95130
  // src/cli/index.ts
95044
- var version2 = true ? "0.6.0" : "0.0.0-dev";
95131
+ var version2 = true ? "0.7.0" : "0.0.0-dev";
95045
95132
  var program2 = new Command();
95046
95133
  program2.name("flow-weaver").description("Flow Weaver Annotations - Compile and validate workflow files").version(version2, "-v, --version", "Output the current version");
95047
95134
  program2.configureOutput({
@@ -133,16 +133,35 @@ export function registerEditorTools(mcp, connection, buffer) {
133
133
  .boolean()
134
134
  .optional()
135
135
  .describe('Include execution trace events (default: true)'),
136
- }, async (args) => {
136
+ }, async (args, extra) => {
137
137
  // When filePath is provided, compile and execute directly (no editor needed)
138
138
  if (args.filePath) {
139
139
  try {
140
140
  const channel = new AgentChannel();
141
141
  const runId = `run-${Date.now()}-${Math.random().toString(36).slice(2)}`;
142
+ // Send progress notifications for trace events when client supports it
143
+ const progressToken = extra._meta?.progressToken;
144
+ let eventCount = 0;
145
+ const onEvent = progressToken
146
+ ? (event) => {
147
+ eventCount++;
148
+ extra.sendNotification({
149
+ method: 'notifications/progress',
150
+ params: {
151
+ progressToken,
152
+ progress: eventCount,
153
+ message: event.type === 'STATUS_CHANGED'
154
+ ? `${event.data?.id ?? ''}: ${event.data?.status ?? ''}`
155
+ : event.type,
156
+ },
157
+ }).catch(() => { });
158
+ }
159
+ : undefined;
142
160
  const execPromise = executeWorkflowFromFile(args.filePath, args.params, {
143
161
  workflowName: args.workflowName,
144
162
  includeTrace: args.includeTrace,
145
163
  agentChannel: channel,
164
+ onEvent,
146
165
  });
147
166
  // Race between workflow completing and workflow pausing
148
167
  const raceResult = await Promise.race([
@@ -4,7 +4,6 @@
4
4
  */
5
5
  import * as path from 'path';
6
6
  import * as fs from 'fs';
7
- import * as os from 'os';
8
7
  import { pathToFileURL } from 'url';
9
8
  import ts from 'typescript';
10
9
  import { compileWorkflow } from '../api/index.js';
@@ -31,7 +30,13 @@ export async function executeWorkflowFromFile(filePath, params, options) {
31
30
  // In-place compilation preserves all functions in the module (node types,
32
31
  // sibling workflows), which is required for workflow composition where one
33
32
  // workflow calls another as a node type.
34
- const tmpBase = path.join(os.tmpdir(), `fw-exec-${Date.now()}-${Math.random().toString(36).slice(2)}`);
33
+ //
34
+ // Temp files are written in the source file's directory (not os.tmpdir())
35
+ // so that ESM module resolution can walk up to the project's node_modules.
36
+ // On Windows, os.tmpdir() is disconnected from the project tree, causing
37
+ // bare import specifiers (e.g. 'zod', 'openai') to fail with MODULE_NOT_FOUND.
38
+ const tmpId = `fw-exec-${Date.now()}-${Math.random().toString(36).slice(2)}`;
39
+ const tmpBase = path.join(path.dirname(resolvedPath), tmpId);
35
40
  const tmpTsFile = `${tmpBase}.ts`;
36
41
  const tmpFile = `${tmpBase}.mjs`;
37
42
  try {
package/dist/validator.js CHANGED
@@ -2,6 +2,19 @@ import { RESERVED_NODE_NAMES, isStartNode, isExitNode, isExecutePort, isReserved
2
2
  import { findClosestMatches } from './utils/string-distance.js';
3
3
  import { parseFunctionSignature } from './jsdoc-port-sync/signature-parser.js';
4
4
  import { checkTypeCompatibilityFromStrings } from './type-checker.js';
5
+ const DOCS_BASE = 'https://docs.flowweaver.dev/reference';
6
+ /** Map error codes to the documentation page that explains how to fix them. */
7
+ const ERROR_DOC_URLS = {
8
+ UNKNOWN_NODE_TYPE: `${DOCS_BASE}/concepts#node-registration`,
9
+ UNKNOWN_SOURCE_PORT: `${DOCS_BASE}/concepts#port-architecture`,
10
+ UNKNOWN_TARGET_PORT: `${DOCS_BASE}/concepts#port-architecture`,
11
+ TYPE_MISMATCH: `${DOCS_BASE}/compilation#type-compatibility`,
12
+ UNREACHABLE_NODE: `${DOCS_BASE}/concepts#graph-structure`,
13
+ MISSING_START_CONNECTION: `${DOCS_BASE}/concepts#start-and-exit`,
14
+ MISSING_EXIT_CONNECTION: `${DOCS_BASE}/concepts#start-and-exit`,
15
+ INFERRED_NODE_TYPE: `${DOCS_BASE}/node-conversion`,
16
+ DUPLICATE_CONNECTION: `${DOCS_BASE}/concepts#connections`,
17
+ };
5
18
  export class WorkflowValidator {
6
19
  errors = [];
7
20
  warnings = [];
@@ -149,6 +162,12 @@ export class WorkflowValidator {
149
162
  return true;
150
163
  });
151
164
  }
165
+ // Attach doc URLs to diagnostics that have mapped error codes
166
+ for (const diag of [...this.errors, ...this.warnings]) {
167
+ if (!diag.docUrl && ERROR_DOC_URLS[diag.code]) {
168
+ diag.docUrl = ERROR_DOC_URLS[diag.code];
169
+ }
170
+ }
152
171
  return {
153
172
  valid: this.errors.length === 0,
154
173
  errors: this.errors,
@@ -215,11 +215,24 @@ Use `@fwImport` to turn npm package functions or local module exports into node
215
215
  */
216
216
  ```
217
217
 
218
- - First identifier: node type name (convention: `npm/pkg/fn` or `local/path/fn`)
219
- - Second identifier: exported function name
220
- - String: package name or relative path
218
+ **Syntax**: `@fwImport <nodeTypeName> <functionName> from "<package-or-path>"`
221
219
 
222
- Imported functions become expression nodes. Port types are inferred from `.d.ts` files when available.
220
+ - **Node type name** (first identifier): used in `@node` declarations. Convention: `npm/pkg/fn` for packages, `local/path/fn` for local modules.
221
+ - **Function name** (second identifier): the actual exported function name to import.
222
+ - **Source** (quoted string): npm package name or relative path to a local module.
223
+
224
+ **Prefix semantics**:
225
+ - `npm/` — resolves to a bare package specifier. The package must be installed in `node_modules`. At compile time, the compiler generates an `import { fn } from "package"` statement in the output.
226
+ - `local/` — resolves to a relative import from the workflow file's directory. Generates `import { fn } from "./path"`.
227
+
228
+ **Type inference**: port types are inferred from the function's TypeScript signature (from `.d.ts` files for npm packages, or from the source for local modules). If type information isn't available, ports default to `ANY`.
229
+
230
+ **What happens at compile time**: the compiler parses the `@fwImport` annotation, resolves the function signature, creates a virtual node type with inferred ports, and emits the corresponding import statement in the generated code. The imported function is called as an expression node — no `execute` parameter, no STEP ports.
231
+
232
+ **Common errors**:
233
+ - Package not installed: `npm install <package>` before compiling.
234
+ - Wrong export name: check the package's exports with your IDE or `npm info <package>`.
235
+ - No type information: install `@types/<package>` for community type definitions.
223
236
 
224
237
  ## Mandatory Signatures
225
238
 
@@ -245,6 +258,33 @@ export function myWorkflow(execute: boolean, params: { inputA: Type }): {...}
245
258
 
246
259
  > **Key difference:** Nodes use direct params, workflows use `params` object.
247
260
 
261
+ ## Node Registration
262
+
263
+ Every node used in a workflow must be explicitly declared with `@node`. The compiler builds a static directed graph from annotations at compile time, so it needs to know about every node before code generation begins. This is different from normal function calls where you just invoke a function directly.
264
+
265
+ Built-in nodes (`delay`, `waitForEvent`, `invokeWorkflow`, `waitForAgent`) are exported from the library but still need explicit declaration in your workflow file. The compiler validates all `@node` references against the set of available node types — functions annotated with `@flowWeaver nodeType` in the same file or imported via `@fwImport`.
266
+
267
+ To use a built-in node, define or import the function and annotate it:
268
+
269
+ ```typescript
270
+ import { waitForEvent } from '@synergenius/flow-weaver/built-in-nodes';
271
+
272
+ /**
273
+ * @flowWeaver nodeType
274
+ * @input eventName - Event to wait for
275
+ * @output eventData - Received event payload
276
+ */
277
+ // (function body provided by the library)
278
+
279
+ /**
280
+ * @flowWeaver workflow
281
+ * @node wait waitForEvent
282
+ * @connect Start.eventName -> wait.eventName
283
+ * @connect wait.eventData -> Exit.data
284
+ */
285
+ export function myWorkflow(execute: boolean, params: { eventName: string }) { ... }
286
+ ```
287
+
248
288
  ## Port Types
249
289
 
250
290
  STRING, NUMBER, BOOLEAN, OBJECT, ARRAY, FUNCTION, ANY, STEP
@@ -169,6 +169,39 @@ flow-weaver describe src/workflows/my-workflow.ts --format mermaid # diagram
169
169
  flow-weaver describe src/workflows/my-workflow.ts --node fetcher1 # focus on a node
170
170
  ```
171
171
 
172
+ ### flow-weaver run --stream vs --trace
173
+
174
+ Both flags give you execution trace data, but in different ways:
175
+
176
+ **`--stream`** writes events to stderr in real-time as nodes execute. Each STATUS_CHANGED event prints the node ID, new status, and duration. Use this for live debugging during development — you can watch the workflow progress node by node.
177
+
178
+ ```bash
179
+ flow-weaver run workflow.ts --stream
180
+ # Output (to stderr):
181
+ # [STATUS_CHANGED] fetcher: → RUNNING
182
+ # [STATUS_CHANGED] fetcher: → SUCCEEDED (142ms)
183
+ # [STATUS_CHANGED] processor: → RUNNING
184
+ # [VARIABLE_SET] processor.result
185
+ # [STATUS_CHANGED] processor: → SUCCEEDED (38ms)
186
+ ```
187
+
188
+ **`--trace`** collects all ExecutionTraceEvent objects during execution and includes them in the output after completion. Use this for post-mortem analysis or programmatic consumption (e.g., in CI or with `--json`).
189
+
190
+ ```bash
191
+ flow-weaver run workflow.ts --trace
192
+ # Shows: "12 events captured" + first 5 events as summary
193
+
194
+ flow-weaver run workflow.ts --trace --json | jq '.traceCount'
195
+ # Outputs: 12
196
+ ```
197
+
198
+ **Combining both**: `--stream --trace` gives you real-time output during execution AND the collected trace array in the result. Useful when you want to watch progress live but also capture the full event log.
199
+
200
+ **When to use which**:
201
+ - Debugging interactively → `--stream`
202
+ - CI pipeline or scripted analysis → `--trace --json`
203
+ - Both → `--stream --trace`
204
+
172
205
  ### Diagnostic Strategy
173
206
 
174
207
  1. **flow-weaver validate** -- Get all errors and warnings. Fix errors first.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synergenius/flow-weaver",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Deterministic workflow compiler for AI agents. Compiles to standalone TypeScript, no runtime dependencies.",
5
5
  "private": false,
6
6
  "type": "module",