@synergenius/flow-weaver 0.22.5 → 0.22.7

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.
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Claude CLI provider — spawns the Claude Code CLI with stream-json output
3
+ * and MCP bridge for tool execution.
4
+ *
5
+ * Adapted from platform's streamClaudeCliChat. Platform-specific dependencies
6
+ * (spawnSandboxed, getBinPath, config) are replaced with injectable options.
7
+ */
8
+ import { spawn as nodeSpawn } from 'node:child_process';
9
+ import { StreamJsonParser } from '../streaming.js';
10
+ import { createMcpBridge } from '../mcp-bridge.js';
11
+ export class ClaudeCliProvider {
12
+ binPath;
13
+ cwd;
14
+ env;
15
+ model;
16
+ mcpConfigPath;
17
+ spawnFn;
18
+ timeout;
19
+ constructor(options = {}) {
20
+ this.binPath = options.binPath ?? 'claude';
21
+ this.cwd = options.cwd ?? process.cwd();
22
+ this.env = options.env ?? process.env;
23
+ this.model = options.model;
24
+ this.mcpConfigPath = options.mcpConfigPath;
25
+ this.spawnFn = options.spawnFn ?? ((cmd, args, opts) => nodeSpawn(cmd, args, { ...opts, stdio: opts.stdio }));
26
+ this.timeout = options.timeout ?? 120_000;
27
+ }
28
+ async *stream(messages, tools, options) {
29
+ const model = options?.model ?? this.model;
30
+ // Format messages into a single prompt for -p mode
31
+ const prompt = formatPrompt(messages);
32
+ const systemPrompt = options?.systemPrompt;
33
+ // Set up MCP bridge for tool access if tools are provided and no config given
34
+ let bridge = null;
35
+ let mcpConfigPath = this.mcpConfigPath;
36
+ if (tools.length > 0 && !mcpConfigPath) {
37
+ // Create a temporary bridge — tool execution is handled by the agent loop,
38
+ // but we still need to advertise tool definitions to the CLI via MCP.
39
+ // The bridge executor won't be called because the CLI handles its own tool
40
+ // loop internally when using MCP tools.
41
+ bridge = await createMcpBridge(tools, async () => ({ result: 'Not implemented', isError: true }));
42
+ mcpConfigPath = bridge.configPath;
43
+ }
44
+ const args = [
45
+ '-p',
46
+ '--verbose',
47
+ '--output-format',
48
+ 'stream-json',
49
+ '--include-partial-messages',
50
+ '--setting-sources',
51
+ 'user,local',
52
+ '--dangerously-skip-permissions',
53
+ '--permission-mode',
54
+ 'bypassPermissions',
55
+ ...(systemPrompt ? ['--system-prompt', systemPrompt] : []),
56
+ ...(mcpConfigPath ? ['--mcp-config', mcpConfigPath, '--strict-mcp-config'] : []),
57
+ ...(model ? ['--model', model] : []),
58
+ ];
59
+ // Spawn the CLI process
60
+ const spawnResult = this.spawnFn(this.binPath, args, { cwd: this.cwd, stdio: ['pipe', 'pipe', 'pipe'], env: this.env });
61
+ const child = 'child' in spawnResult ? spawnResult.child : spawnResult;
62
+ const spawnCleanup = 'cleanup' in spawnResult ? spawnResult.cleanup : undefined;
63
+ const signal = options?.signal;
64
+ if (signal) {
65
+ signal.addEventListener('abort', () => {
66
+ child.kill('SIGTERM');
67
+ setTimeout(() => {
68
+ if (!child.killed)
69
+ child.kill('SIGKILL');
70
+ }, 2000);
71
+ }, { once: true });
72
+ }
73
+ // Timeout
74
+ const timer = setTimeout(() => {
75
+ child.kill('SIGTERM');
76
+ }, this.timeout);
77
+ child.stdin.write(prompt);
78
+ child.stdin.end();
79
+ // Collect events from the parser
80
+ const events = [];
81
+ let done = false;
82
+ const parser = new StreamJsonParser((event) => {
83
+ events.push(event);
84
+ });
85
+ let stderrBuf = '';
86
+ child.stderr.on('data', (chunk) => {
87
+ stderrBuf += chunk.toString();
88
+ });
89
+ let stdoutBuffer = '';
90
+ child.stdout.on('data', (chunk) => {
91
+ stdoutBuffer += chunk.toString();
92
+ const lines = stdoutBuffer.split('\n');
93
+ stdoutBuffer = lines.pop() || '';
94
+ for (const line of lines) {
95
+ parser.feed(line);
96
+ }
97
+ });
98
+ child.on('close', (code) => {
99
+ clearTimeout(timer);
100
+ // Process remaining buffer
101
+ if (stdoutBuffer.trim()) {
102
+ parser.feed(stdoutBuffer);
103
+ }
104
+ if (code !== 0 && !parser.hasText && stderrBuf.trim()) {
105
+ events.push({ type: 'text_delta', text: `Claude CLI error: ${stderrBuf.trim().slice(0, 500)}` });
106
+ }
107
+ if (!events.some((e) => e.type === 'message_stop')) {
108
+ events.push({ type: 'message_stop', finishReason: code === 0 ? 'stop' : 'error' });
109
+ }
110
+ done = true;
111
+ });
112
+ child.on('error', () => {
113
+ clearTimeout(timer);
114
+ events.push({ type: 'message_stop', finishReason: 'error' });
115
+ done = true;
116
+ });
117
+ // Yield events as they become available
118
+ try {
119
+ while (!done || events.length > 0) {
120
+ if (events.length > 0) {
121
+ yield events.shift();
122
+ }
123
+ else {
124
+ await new Promise((r) => setTimeout(r, 10));
125
+ }
126
+ }
127
+ }
128
+ finally {
129
+ bridge?.cleanup();
130
+ spawnCleanup?.();
131
+ }
132
+ }
133
+ }
134
+ /**
135
+ * Format agent messages into a single prompt string for the CLI's -p mode.
136
+ */
137
+ function formatPrompt(messages) {
138
+ const parts = [];
139
+ for (const msg of messages) {
140
+ if (msg.role === 'user') {
141
+ parts.push(`User: ${typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)}\n`);
142
+ }
143
+ else if (msg.role === 'assistant') {
144
+ parts.push(`Assistant: ${typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)}\n`);
145
+ }
146
+ else if (msg.role === 'tool') {
147
+ parts.push(`Tool result (${msg.toolCallId}): ${typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)}\n`);
148
+ }
149
+ }
150
+ return parts.join('\n');
151
+ }
152
+ export function createClaudeCliProvider(options) {
153
+ return new ClaudeCliProvider(options);
154
+ }
155
+ //# sourceMappingURL=claude-cli.js.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * StreamJsonParser — parses Claude CLI stream-json NDJSON output into
3
+ * typed StreamEvent values.
4
+ *
5
+ * Extracted from the duplicated parsing logic in platform's claude.ts
6
+ * and cli-session.ts. Handles:
7
+ * - stream_event wrapper unwrapping
8
+ * - content_block lifecycle (text, tool_use, thinking)
9
+ * - Tool argument accumulation (input_json_delta → tool_use_end)
10
+ * - Tool result blocks from CLI's internal tool loop (user events)
11
+ * - Usage tracking from message_start, message_delta, and result
12
+ * - result event as text fallback and turn boundary
13
+ *
14
+ * The parser does NOT decide turn boundaries — consumers (one-shot vs
15
+ * persistent session) handle that differently.
16
+ */
17
+ import type { StreamEvent } from './types.js';
18
+ export type EventCallback = (event: StreamEvent) => void;
19
+ export declare class StreamJsonParser {
20
+ private pushEvent;
21
+ private hasAssistantText;
22
+ private insideToolUse;
23
+ private activeToolJsonChunks;
24
+ constructor(pushEvent: EventCallback);
25
+ /** Reset per-turn state. Call before starting a new turn in persistent sessions. */
26
+ reset(): void;
27
+ /** Whether any text_delta events have been emitted this turn. */
28
+ get hasText(): boolean;
29
+ /**
30
+ * Feed a single NDJSON line (without trailing newline).
31
+ * Parses and emits StreamEvent(s) via the callback.
32
+ */
33
+ feed(line: string): void;
34
+ private processEvent;
35
+ }
36
+ //# sourceMappingURL=streaming.d.ts.map
@@ -0,0 +1,183 @@
1
+ /**
2
+ * StreamJsonParser — parses Claude CLI stream-json NDJSON output into
3
+ * typed StreamEvent values.
4
+ *
5
+ * Extracted from the duplicated parsing logic in platform's claude.ts
6
+ * and cli-session.ts. Handles:
7
+ * - stream_event wrapper unwrapping
8
+ * - content_block lifecycle (text, tool_use, thinking)
9
+ * - Tool argument accumulation (input_json_delta → tool_use_end)
10
+ * - Tool result blocks from CLI's internal tool loop (user events)
11
+ * - Usage tracking from message_start, message_delta, and result
12
+ * - result event as text fallback and turn boundary
13
+ *
14
+ * The parser does NOT decide turn boundaries — consumers (one-shot vs
15
+ * persistent session) handle that differently.
16
+ */
17
+ export class StreamJsonParser {
18
+ pushEvent;
19
+ hasAssistantText = false;
20
+ insideToolUse = false;
21
+ activeToolJsonChunks = new Map();
22
+ constructor(pushEvent) {
23
+ this.pushEvent = pushEvent;
24
+ }
25
+ /** Reset per-turn state. Call before starting a new turn in persistent sessions. */
26
+ reset() {
27
+ this.hasAssistantText = false;
28
+ this.insideToolUse = false;
29
+ this.activeToolJsonChunks.clear();
30
+ }
31
+ /** Whether any text_delta events have been emitted this turn. */
32
+ get hasText() {
33
+ return this.hasAssistantText;
34
+ }
35
+ /**
36
+ * Feed a single NDJSON line (without trailing newline).
37
+ * Parses and emits StreamEvent(s) via the callback.
38
+ */
39
+ feed(line) {
40
+ if (!line.trim())
41
+ return;
42
+ let event;
43
+ try {
44
+ event = JSON.parse(line);
45
+ }
46
+ catch {
47
+ // Non-JSON line — only use as text if no other source is available
48
+ if (line.trim() && !this.hasAssistantText) {
49
+ this.pushEvent({ type: 'text_delta', text: line });
50
+ }
51
+ return;
52
+ }
53
+ // Unwrap stream_event wrapper (--include-partial-messages wraps API events)
54
+ if (event.type === 'stream_event' && event.event) {
55
+ event = event.event;
56
+ }
57
+ this.processEvent(event);
58
+ }
59
+ processEvent(event) {
60
+ const block = event.content_block;
61
+ const delta = event.delta;
62
+ // --- content_block_start ---
63
+ if (event.type === 'content_block_start' && block?.type === 'tool_use') {
64
+ this.insideToolUse = true;
65
+ const id = block.id || `cli-tool-${Date.now()}`;
66
+ const name = block.name || 'unknown';
67
+ this.pushEvent({ type: 'tool_use_start', id, name });
68
+ this.activeToolJsonChunks.set(id, { name, chunks: [] });
69
+ return;
70
+ }
71
+ if (event.type === 'content_block_start' && block?.type === 'text') {
72
+ this.insideToolUse = false;
73
+ return;
74
+ }
75
+ // --- content_block_delta ---
76
+ if (event.type === 'content_block_delta' && delta?.type === 'input_json_delta') {
77
+ const lastTool = [...this.activeToolJsonChunks.entries()].pop();
78
+ if (lastTool)
79
+ lastTool[1].chunks.push(delta.partial_json || '');
80
+ return;
81
+ }
82
+ if (event.type === 'content_block_delta' && delta?.type === 'thinking_delta' && delta?.thinking) {
83
+ this.pushEvent({ type: 'thinking_delta', text: delta.thinking });
84
+ return;
85
+ }
86
+ if (event.type === 'content_block_delta' && delta?.text && !this.insideToolUse) {
87
+ this.hasAssistantText = true;
88
+ this.pushEvent({ type: 'text_delta', text: delta.text });
89
+ return;
90
+ }
91
+ // --- content_block_stop ---
92
+ if (event.type === 'content_block_stop' && this.insideToolUse) {
93
+ const lastTool = [...this.activeToolJsonChunks.entries()].pop();
94
+ if (lastTool) {
95
+ const [id, { chunks }] = lastTool;
96
+ let args = {};
97
+ try {
98
+ args = JSON.parse(chunks.join(''));
99
+ }
100
+ catch {
101
+ /* malformed */
102
+ }
103
+ this.pushEvent({ type: 'tool_use_end', id, arguments: args });
104
+ this.activeToolJsonChunks.delete(id);
105
+ }
106
+ this.insideToolUse = false;
107
+ return;
108
+ }
109
+ // --- user event with tool_result blocks (CLI's internal tool loop) ---
110
+ if (event.type === 'user' && event.message?.content) {
111
+ const content = event.message.content;
112
+ for (const contentBlock of content) {
113
+ if (contentBlock.type === 'tool_result' && contentBlock.tool_use_id) {
114
+ const text = Array.isArray(contentBlock.content)
115
+ ? contentBlock.content
116
+ .map((c) => c.text || '')
117
+ .join('')
118
+ : String(contentBlock.content || '');
119
+ this.pushEvent({
120
+ type: 'tool_result',
121
+ id: contentBlock.tool_use_id,
122
+ result: text,
123
+ isError: !!contentBlock.is_error,
124
+ });
125
+ }
126
+ }
127
+ return;
128
+ }
129
+ // --- message_start (usage) ---
130
+ if (event.type === 'message_start' && event.message?.usage) {
131
+ const usage = event.message.usage;
132
+ this.pushEvent({
133
+ type: 'usage',
134
+ promptTokens: usage.input_tokens ?? 0,
135
+ completionTokens: usage.output_tokens ?? 0,
136
+ });
137
+ return;
138
+ }
139
+ // --- message_delta (usage) ---
140
+ if (event.type === 'message_delta' && event.usage) {
141
+ const usage = event.usage;
142
+ this.pushEvent({
143
+ type: 'usage',
144
+ promptTokens: 0,
145
+ completionTokens: usage.output_tokens ?? 0,
146
+ });
147
+ return;
148
+ }
149
+ // --- message_stop ---
150
+ if (event.type === 'message_stop') {
151
+ this.pushEvent({ type: 'message_stop', finishReason: 'stop' });
152
+ return;
153
+ }
154
+ // --- result event (CLI turn boundary) ---
155
+ if (event.type === 'result') {
156
+ if (event.is_error) {
157
+ this.pushEvent({ type: 'message_stop', finishReason: 'error' });
158
+ return;
159
+ }
160
+ // result text is a fallback — only use if content_block_delta never fired
161
+ if (event.result && !this.hasAssistantText) {
162
+ this.pushEvent({ type: 'text_delta', text: event.result });
163
+ }
164
+ if (event.usage) {
165
+ const usage = event.usage;
166
+ this.pushEvent({
167
+ type: 'usage',
168
+ promptTokens: usage.input_tokens ?? 0,
169
+ completionTokens: usage.output_tokens ?? 0,
170
+ });
171
+ }
172
+ this.pushEvent({ type: 'message_stop', finishReason: 'stop' });
173
+ return;
174
+ }
175
+ // --- assistant event (auth failure detection) ---
176
+ if (event.type === 'assistant') {
177
+ if (event.error === 'authentication_failed') {
178
+ this.pushEvent({ type: 'message_stop', finishReason: 'error' });
179
+ }
180
+ }
181
+ }
182
+ }
183
+ //# sourceMappingURL=streaming.js.map
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Shared types for the agent loop, providers, and MCP bridge.
3
+ *
4
+ * All types are pure — no runtime imports, no side effects.
5
+ */
6
+ import type { ChildProcess } from 'node:child_process';
7
+ export type StreamEvent = {
8
+ type: 'text_delta';
9
+ text: string;
10
+ } | {
11
+ type: 'thinking_delta';
12
+ text: string;
13
+ } | {
14
+ type: 'tool_use_start';
15
+ id: string;
16
+ name: string;
17
+ } | {
18
+ type: 'tool_use_delta';
19
+ id: string;
20
+ partialJson: string;
21
+ } | {
22
+ type: 'tool_use_end';
23
+ id: string;
24
+ arguments: Record<string, unknown>;
25
+ } | {
26
+ type: 'tool_result';
27
+ id: string;
28
+ result: string;
29
+ isError: boolean;
30
+ } | {
31
+ type: 'message_stop';
32
+ finishReason: 'stop' | 'tool_calls' | 'length' | 'error';
33
+ } | {
34
+ type: 'usage';
35
+ promptTokens: number;
36
+ completionTokens: number;
37
+ };
38
+ export interface AgentMessage {
39
+ role: 'user' | 'assistant' | 'tool';
40
+ content: string | Array<Record<string, unknown>>;
41
+ toolCalls?: Array<{
42
+ id: string;
43
+ name: string;
44
+ arguments: Record<string, unknown>;
45
+ }>;
46
+ toolCallId?: string;
47
+ }
48
+ export interface ToolDefinition {
49
+ name: string;
50
+ description: string;
51
+ inputSchema: {
52
+ type: string;
53
+ properties: Record<string, unknown>;
54
+ required?: string[];
55
+ };
56
+ }
57
+ export type ToolExecutor = (name: string, args: Record<string, unknown>) => Promise<{
58
+ result: string;
59
+ isError: boolean;
60
+ }>;
61
+ export interface ToolEvent {
62
+ type: 'tool_call_start' | 'tool_call_result';
63
+ name: string;
64
+ args?: Record<string, unknown>;
65
+ result?: string;
66
+ isError?: boolean;
67
+ }
68
+ export interface StreamOptions {
69
+ systemPrompt?: string;
70
+ model?: string;
71
+ maxTokens?: number;
72
+ signal?: AbortSignal;
73
+ }
74
+ export interface AgentProvider {
75
+ stream(messages: AgentMessage[], tools: ToolDefinition[], options?: StreamOptions): AsyncGenerator<StreamEvent>;
76
+ }
77
+ export interface McpBridge {
78
+ /** Path to the MCP config JSON file — pass to --mcp-config */
79
+ configPath: string;
80
+ /** Update the executor and event callback for a new request */
81
+ setHandlers: (executor: ToolExecutor, onToolEvent?: (event: ToolEvent) => void) => void;
82
+ /** Tear down the socket server and remove temp files */
83
+ cleanup: () => void;
84
+ }
85
+ export interface AgentLoopOptions {
86
+ systemPrompt?: string;
87
+ maxIterations?: number;
88
+ maxTokens?: number;
89
+ model?: string;
90
+ signal?: AbortSignal;
91
+ onToolEvent?: (event: ToolEvent) => void;
92
+ onStreamEvent?: (event: StreamEvent) => void;
93
+ logger?: Logger;
94
+ }
95
+ export interface AgentLoopResult {
96
+ success: boolean;
97
+ summary: string;
98
+ messages: AgentMessage[];
99
+ toolCallCount: number;
100
+ usage: {
101
+ promptTokens: number;
102
+ completionTokens: number;
103
+ };
104
+ }
105
+ export type SpawnFn = (command: string, args: string[], options: {
106
+ cwd: string;
107
+ stdio: string[];
108
+ env: NodeJS.ProcessEnv;
109
+ }) => ChildProcess | {
110
+ child: ChildProcess;
111
+ cleanup?: () => void;
112
+ };
113
+ export interface ClaudeCliProviderOptions {
114
+ /** Absolute path to the claude binary. Defaults to 'claude' (found via PATH). */
115
+ binPath?: string;
116
+ /** Working directory for the CLI process. */
117
+ cwd?: string;
118
+ /** Environment variables for the CLI process. */
119
+ env?: NodeJS.ProcessEnv;
120
+ /** Model override. */
121
+ model?: string;
122
+ /** Pre-configured MCP config path (skips auto-bridge creation). */
123
+ mcpConfigPath?: string;
124
+ /** Custom spawn function. Defaults to child_process.spawn. */
125
+ spawnFn?: SpawnFn;
126
+ /** CLI timeout in milliseconds. Defaults to 120000. */
127
+ timeout?: number;
128
+ }
129
+ export interface CliSessionOptions {
130
+ /** Absolute path to the claude binary. */
131
+ binPath: string;
132
+ /** Working directory for the CLI process. */
133
+ cwd: string;
134
+ /** Environment variables for the CLI process. */
135
+ env?: NodeJS.ProcessEnv;
136
+ /** Model to use. */
137
+ model: string;
138
+ /** Pre-configured MCP config path. */
139
+ mcpConfigPath?: string;
140
+ /** Custom spawn function. Defaults to child_process.spawn. */
141
+ spawnFn?: SpawnFn;
142
+ /** Idle timeout in milliseconds. Defaults to 600000 (10 minutes). */
143
+ idleTimeout?: number;
144
+ /** Logger instance. */
145
+ logger?: Logger;
146
+ }
147
+ export interface Logger {
148
+ info(msg: string, data?: unknown): void;
149
+ warn(msg: string, data?: unknown): void;
150
+ error(msg: string, data?: unknown): void;
151
+ }
152
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Shared types for the agent loop, providers, and MCP bridge.
3
+ *
4
+ * All types are pure — no runtime imports, no side effects.
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=types.js.map
@@ -9886,7 +9886,7 @@ var VERSION;
9886
9886
  var init_generated_version = __esm({
9887
9887
  "src/generated-version.ts"() {
9888
9888
  "use strict";
9889
- VERSION = "0.22.5";
9889
+ VERSION = "0.22.7";
9890
9890
  }
9891
9891
  });
9892
9892
 
@@ -10791,16 +10791,35 @@ function buildNodeArgumentsWithContext(opts) {
10791
10791
  } else if (skipPorts?.has("execute")) {
10792
10792
  args.push(`${safeId}_execute`);
10793
10793
  } else if (executeConnections.length > 0) {
10794
- const conn = executeConnections[0];
10795
- const sourceNode = conn.from.node;
10796
- const sourcePort = conn.from.port;
10797
10794
  const varName = `${safeId}_execute`;
10798
- const sourceIdx = isStartNode(sourceNode) ? "startIdx" : `${toValidIdentifier(sourceNode)}Idx`;
10799
- const isConstSource = isStartNode(sourceNode) || sourceNode === instanceParent;
10800
- const nonNullAssert = isConstSource ? "" : "!";
10801
- lines.push(
10802
- `${indent}const ${varName} = ${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx}${nonNullAssert} }) as boolean;`
10803
- );
10795
+ if (executeConnections.length === 1) {
10796
+ const conn = executeConnections[0];
10797
+ const sourceNode = conn.from.node;
10798
+ const sourcePort = conn.from.port;
10799
+ const sourceIdx = isStartNode(sourceNode) ? "startIdx" : `${toValidIdentifier(sourceNode)}Idx`;
10800
+ const isConstSource = isStartNode(sourceNode) || sourceNode === instanceParent;
10801
+ if (isConstSource) {
10802
+ lines.push(
10803
+ `${indent}const ${varName} = ${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx} }) as boolean;`
10804
+ );
10805
+ } else {
10806
+ lines.push(
10807
+ `${indent}const ${varName} = ${sourceIdx} !== undefined ? ${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx} }) as boolean : false;`
10808
+ );
10809
+ }
10810
+ } else {
10811
+ const parts = executeConnections.map((conn) => {
10812
+ const sourceNode = conn.from.node;
10813
+ const sourcePort = conn.from.port;
10814
+ const sourceIdx = isStartNode(sourceNode) ? "startIdx" : `${toValidIdentifier(sourceNode)}Idx`;
10815
+ const isConstSource = isStartNode(sourceNode) || sourceNode === instanceParent;
10816
+ if (isConstSource) {
10817
+ return `(${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx} }) as boolean)`;
10818
+ }
10819
+ return `(${sourceIdx} !== undefined ? ${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx} }) as boolean : false)`;
10820
+ });
10821
+ lines.push(`${indent}const ${varName} = ${parts.join(" || ")};`);
10822
+ }
10804
10823
  if (emitInputEvents) {
10805
10824
  lines.push(
10806
10825
  `${indent}${setCall}({ id: '${id}', portName: 'execute', executionIndex: ${safeId}Idx, nodeTypeName: '${effectiveNodeTypeName}' }, ${varName});`
@@ -94920,7 +94939,7 @@ var {
94920
94939
  // src/cli/index.ts
94921
94940
  init_logger();
94922
94941
  init_error_utils();
94923
- var version2 = true ? "0.22.5" : "0.0.0-dev";
94942
+ var version2 = true ? "0.22.7" : "0.0.0-dev";
94924
94943
  var program2 = new Command();
94925
94944
  program2.name("fw").description("Flow Weaver Annotations - Compile and validate workflow files").option("-v, --version", "Output the current version").option("--no-color", "Disable colors").option("--color", "Force colors").on("option:version", () => {
94926
94945
  logger.banner(version2);
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.22.5";
1
+ export declare const VERSION = "0.22.7";
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.22.5';
2
+ export const VERSION = '0.22.7';
3
3
  //# sourceMappingURL=generated-version.js.map
@@ -172,16 +172,36 @@ export function buildNodeArgumentsWithContext(opts) {
172
172
  args.push(`${safeId}_execute`);
173
173
  }
174
174
  else if (executeConnections.length > 0) {
175
- // Execute port has a connection - use it
176
- const conn = executeConnections[0];
177
- const sourceNode = conn.from.node;
178
- const sourcePort = conn.from.port;
175
+ // Execute port has connections - use them
179
176
  const varName = `${safeId}_execute`;
180
- // startIdx is const so no ! needed; parent scope node is also const; other node indices are let so need !
181
- const sourceIdx = isStartNode(sourceNode) ? 'startIdx' : `${toValidIdentifier(sourceNode)}Idx`;
182
- const isConstSource = isStartNode(sourceNode) || sourceNode === instanceParent;
183
- const nonNullAssert = isConstSource ? '' : '!';
184
- lines.push(`${indent}const ${varName} = ${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx}${nonNullAssert} }) as boolean;`);
177
+ if (executeConnections.length === 1) {
178
+ const conn = executeConnections[0];
179
+ const sourceNode = conn.from.node;
180
+ const sourcePort = conn.from.port;
181
+ const sourceIdx = isStartNode(sourceNode) ? 'startIdx' : `${toValidIdentifier(sourceNode)}Idx`;
182
+ const isConstSource = isStartNode(sourceNode) || sourceNode === instanceParent;
183
+ if (isConstSource) {
184
+ lines.push(`${indent}const ${varName} = ${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx} }) as boolean;`);
185
+ }
186
+ else {
187
+ // Non-const source may be undefined (CANCELLED branch) — guard with false default
188
+ lines.push(`${indent}const ${varName} = ${sourceIdx} !== undefined ? ${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx} }) as boolean : false;`);
189
+ }
190
+ }
191
+ else {
192
+ // Multiple execute connections — coalesce with ||, each guarded
193
+ const parts = executeConnections.map((conn) => {
194
+ const sourceNode = conn.from.node;
195
+ const sourcePort = conn.from.port;
196
+ const sourceIdx = isStartNode(sourceNode) ? 'startIdx' : `${toValidIdentifier(sourceNode)}Idx`;
197
+ const isConstSource = isStartNode(sourceNode) || sourceNode === instanceParent;
198
+ if (isConstSource) {
199
+ return `(${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx} }) as boolean)`;
200
+ }
201
+ return `(${sourceIdx} !== undefined ? ${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx} }) as boolean : false)`;
202
+ });
203
+ lines.push(`${indent}const ${varName} = ${parts.join(' || ')};`);
204
+ }
185
205
  // Emit VARIABLE_SET for execute input port
186
206
  if (emitInputEvents) {
187
207
  lines.push(`${indent}${setCall}({ id: '${id}', portName: 'execute', executionIndex: ${safeId}Idx, nodeTypeName: '${effectiveNodeTypeName}' }, ${varName});`);
@@ -814,7 +814,9 @@ isAsync = false) {
814
814
  if (!instance)
815
815
  return;
816
816
  const safeId = toValidIdentifier(instanceId);
817
- // Add execution index for this skipped node so the event has a valid reference
817
+ // Use const (block-scoped) intentionally the outer `let ${safeId}Idx`
818
+ // stays undefined, which signals to downstream guards that this node was
819
+ // CANCELLED and its data ports should not be read.
818
820
  lines.push(`${indent}const ${safeId}Idx = ${ctxVar}.addExecution('${instanceId}');`);
819
821
  // Set STEP port variables so downstream nodes reading onSuccess/onFailure
820
822
  // from this cancelled node don't crash with "Variable not found".