@synergenius/flow-weaver 0.10.11 → 0.11.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 (49) hide show
  1. package/dist/api/generate-in-place.js +5 -4
  2. package/dist/api/inline-runtime.js +42 -0
  3. package/dist/cli/commands/run.d.ts +8 -0
  4. package/dist/cli/commands/run.js +396 -4
  5. package/dist/cli/commands/tunnel.d.ts +7 -8
  6. package/dist/cli/commands/tunnel.js +27 -64
  7. package/dist/cli/flow-weaver.mjs +59844 -54301
  8. package/dist/cli/index.js +6 -2
  9. package/dist/cli/tunnel/dispatch.d.ts +18 -0
  10. package/dist/cli/tunnel/dispatch.js +36 -0
  11. package/dist/cli/tunnel/file-lock.d.ts +9 -0
  12. package/dist/cli/tunnel/file-lock.js +36 -0
  13. package/dist/cli/tunnel/handlers/ast-ops.d.ts +10 -0
  14. package/dist/cli/tunnel/handlers/ast-ops.js +252 -0
  15. package/dist/cli/tunnel/handlers/execution.d.ts +7 -0
  16. package/dist/cli/tunnel/handlers/execution.js +89 -0
  17. package/dist/cli/tunnel/handlers/file-ops.d.ts +7 -0
  18. package/dist/cli/tunnel/handlers/file-ops.js +204 -0
  19. package/dist/cli/tunnel/handlers/mutations.d.ts +7 -0
  20. package/dist/cli/tunnel/handlers/mutations.js +285 -0
  21. package/dist/cli/tunnel/handlers/stubs.d.ts +7 -0
  22. package/dist/cli/tunnel/handlers/stubs.js +141 -0
  23. package/dist/cli/tunnel/handlers/templates.d.ts +7 -0
  24. package/dist/cli/tunnel/handlers/templates.js +123 -0
  25. package/dist/cli/tunnel/path-resolver.d.ts +17 -0
  26. package/dist/cli/tunnel/path-resolver.js +52 -0
  27. package/dist/doc-metadata/extractors/mcp-tools.js +189 -0
  28. package/dist/doc-metadata/types.d.ts +1 -1
  29. package/dist/generator/unified.js +112 -35
  30. package/dist/mcp/debug-session.d.ts +30 -0
  31. package/dist/mcp/debug-session.js +25 -0
  32. package/dist/mcp/index.d.ts +1 -0
  33. package/dist/mcp/index.js +1 -0
  34. package/dist/mcp/server.js +2 -0
  35. package/dist/mcp/tools-debug.d.ts +3 -0
  36. package/dist/mcp/tools-debug.js +451 -0
  37. package/dist/mcp/workflow-executor.d.ts +2 -0
  38. package/dist/mcp/workflow-executor.js +12 -2
  39. package/dist/runtime/ExecutionContext.d.ts +19 -0
  40. package/dist/runtime/ExecutionContext.js +43 -0
  41. package/dist/runtime/checkpoint.d.ts +84 -0
  42. package/dist/runtime/checkpoint.js +225 -0
  43. package/dist/runtime/debug-controller.d.ts +110 -0
  44. package/dist/runtime/debug-controller.js +247 -0
  45. package/dist/runtime/index.d.ts +4 -0
  46. package/dist/runtime/index.js +2 -0
  47. package/docs/reference/cli-reference.md +9 -1
  48. package/docs/reference/debugging.md +152 -5
  49. package/package.json +1 -1
@@ -114,15 +114,16 @@ export function generateInPlace(sourceCode, ast, options = {}) {
114
114
  // If any node is async, force async (even if source isn't marked async)
115
115
  const nodesRequireAsync = shouldWorkflowBeAsync(ast, ast.nodeTypes);
116
116
  const sourceIsAsync = detectFunctionIsAsync(result, ast.functionName);
117
- const isAsync = nodesRequireAsync || sourceIsAsync;
118
- // Add async keyword to source if nodes require it but source doesn't have it
119
- const asyncSigResult = ensureAsyncKeyword(result, ast.functionName, nodesRequireAsync);
117
+ const forceAsync = nodesRequireAsync;
118
+ const isAsync = forceAsync || sourceIsAsync;
119
+ // Add async keyword to source if nodes or debug hooks require it
120
+ const asyncSigResult = ensureAsyncKeyword(result, ast.functionName, forceAsync);
120
121
  if (asyncSigResult.changed) {
121
122
  result = asyncSigResult.code;
122
123
  hasChanges = true;
123
124
  }
124
125
  // Step 5b: Wrap return type in Promise<T> when async was added
125
- const returnTypeResult = ensurePromiseReturnType(result, ast.functionName, nodesRequireAsync);
126
+ const returnTypeResult = ensurePromiseReturnType(result, ast.functionName, forceAsync);
126
127
  if (returnTypeResult.changed) {
127
128
  result = returnTypeResult.code;
128
129
  hasChanges = true;
@@ -89,6 +89,12 @@ export function generateInlineRuntime(production, exportClasses = false) {
89
89
  // (e.g., passed as a function parameter or injected by execution harness)
90
90
  lines.push('declare const __flowWeaverDebugger__: TDebugger | undefined;');
91
91
  lines.push('');
92
+ // Debug controller type for step-through debugging and checkpoint/resume
93
+ lines.push('type TDebugController = {');
94
+ lines.push(' beforeNode(nodeId: string, ctx: GeneratedExecutionContext): Promise<boolean> | boolean;');
95
+ lines.push(' afterNode(nodeId: string, ctx: GeneratedExecutionContext): Promise<void> | void;');
96
+ lines.push('};');
97
+ lines.push('');
92
98
  }
93
99
  // Declare __abortSignal__ so TypeScript knows it might exist at runtime
94
100
  // (passed as a function parameter for cancellation support)
@@ -420,6 +426,42 @@ export function generateInlineRuntime(production, exportClasses = false) {
420
426
  lines.push(' // No-op in production mode');
421
427
  lines.push(' }');
422
428
  }
429
+ // Serialize/restore methods (dev mode only, used by debug controller and checkpointing)
430
+ if (!production) {
431
+ lines.push('');
432
+ lines.push(' serialize(): {');
433
+ lines.push(' variables: Record<string, unknown>;');
434
+ lines.push(' executions: Record<string, ExecutionInfo>;');
435
+ lines.push(' executionCounter: number;');
436
+ lines.push(' nodeExecutionCounts: Record<string, number>;');
437
+ lines.push(' } {');
438
+ lines.push(' const vars: Record<string, unknown> = {};');
439
+ lines.push(' for (const [key, value] of this.variables) {');
440
+ lines.push(' if (typeof value === "function") {');
441
+ lines.push(' try { vars[key] = (value as () => unknown)(); } catch { vars[key] = value; }');
442
+ lines.push(' } else {');
443
+ lines.push(' vars[key] = value;');
444
+ lines.push(' }');
445
+ lines.push(' }');
446
+ lines.push(' const execs: Record<string, ExecutionInfo> = {};');
447
+ lines.push(' for (const [key, info] of this.executions) { execs[key] = { ...info }; }');
448
+ lines.push(' const nodeCounts: Record<string, number> = {};');
449
+ lines.push(' for (const [key, count] of this.nodeExecutionIndices) { nodeCounts[key] = count; }');
450
+ lines.push(' return { variables: vars, executions: execs, executionCounter: this.executionCounter, nodeExecutionCounts: nodeCounts };');
451
+ lines.push(' }');
452
+ lines.push('');
453
+ lines.push(' restore(data: {');
454
+ lines.push(' variables: Record<string, unknown>;');
455
+ lines.push(' executions: Record<string, ExecutionInfo>;');
456
+ lines.push(' executionCounter: number;');
457
+ lines.push(' nodeExecutionCounts: Record<string, number>;');
458
+ lines.push(' }): void {');
459
+ lines.push(' this.variables = new Map(Object.entries(data.variables));');
460
+ lines.push(' this.executions = new Map(Object.entries(data.executions));');
461
+ lines.push(' this.executionCounter = data.executionCounter;');
462
+ lines.push(' this.nodeExecutionIndices = new Map(Object.entries(data.nodeExecutionCounts));');
463
+ lines.push(' }');
464
+ }
423
465
  lines.push('}');
424
466
  lines.push('');
425
467
  return lines.join('\n');
@@ -23,6 +23,14 @@ export interface RunOptions {
23
23
  mocks?: string;
24
24
  /** Path to JSON file containing mock config for built-in nodes */
25
25
  mocksFile?: string;
26
+ /** Start in step-through debug mode */
27
+ debug?: boolean;
28
+ /** Enable checkpointing to disk after each node */
29
+ checkpoint?: boolean;
30
+ /** Resume from a checkpoint file (true for auto-detect, or a file path) */
31
+ resume?: boolean | string;
32
+ /** Initial breakpoint node IDs */
33
+ breakpoint?: string[];
26
34
  }
27
35
  /**
28
36
  * Execute a workflow file and output the result.
@@ -6,6 +6,9 @@ import * as fs from 'fs';
6
6
  import * as readline from 'readline';
7
7
  import { executeWorkflowFromFile } from '../../mcp/workflow-executor.js';
8
8
  import { AgentChannel } from '../../mcp/agent-channel.js';
9
+ import { DebugController } from '../../runtime/debug-controller.js';
10
+ import { CheckpointWriter, loadCheckpoint, findLatestCheckpoint } from '../../runtime/checkpoint.js';
11
+ import { getTopologicalOrder } from '../../api/query.js';
9
12
  import { logger } from '../utils/logger.js';
10
13
  import { getFriendlyError } from '../../friendly-errors.js';
11
14
  import { getErrorMessage } from '../../utils/error-utils.js';
@@ -103,6 +106,46 @@ export async function runCommand(input, options) {
103
106
  }, options.timeout);
104
107
  }
105
108
  try {
109
+ // Handle --resume: load checkpoint and set up skip nodes
110
+ let resumeSkipNodes;
111
+ let resumeCheckpointPath;
112
+ let resumeParams;
113
+ let resumeWorkflowName;
114
+ let resumeExecutionOrder;
115
+ let resumeRerunNodes;
116
+ let resumeStale = false;
117
+ if (options.resume) {
118
+ const checkpointPath = typeof options.resume === 'string'
119
+ ? options.resume
120
+ : findLatestCheckpoint(filePath, options.workflow);
121
+ if (!checkpointPath) {
122
+ throw new Error(`No checkpoint file found for ${filePath}. ` +
123
+ 'Checkpoints are created when running with --checkpoint.');
124
+ }
125
+ const { data, stale, rerunNodes, skipNodes } = loadCheckpoint(checkpointPath, filePath);
126
+ resumeSkipNodes = skipNodes;
127
+ resumeCheckpointPath = checkpointPath;
128
+ resumeParams = data.params;
129
+ resumeWorkflowName = data.workflowName;
130
+ resumeExecutionOrder = data.executionOrder;
131
+ resumeRerunNodes = rerunNodes;
132
+ resumeStale = stale;
133
+ // Use checkpoint params if none provided
134
+ if (Object.keys(params).length === 0) {
135
+ params = data.params;
136
+ }
137
+ if (!options.json) {
138
+ const skipped = data.completedNodes.length - rerunNodes.length;
139
+ logger.info(`Resuming from checkpoint: ${checkpointPath}`);
140
+ logger.info(`Skipping ${skipped} completed nodes`);
141
+ if (rerunNodes.length > 0) {
142
+ logger.info(`Re-running ${rerunNodes.length} nodes: ${rerunNodes.join(', ')}`);
143
+ }
144
+ if (stale) {
145
+ logger.warn('Workflow file has changed since checkpoint was written.');
146
+ }
147
+ }
148
+ }
106
149
  // Determine trace inclusion:
107
150
  // - If --production is set, no trace (unless --trace explicitly set)
108
151
  // - If --trace is set, include trace
@@ -111,6 +154,37 @@ export async function runCommand(input, options) {
111
154
  if (!options.json && mocks) {
112
155
  logger.info('Running with mock data');
113
156
  }
157
+ // Set up debug controller if --debug, --checkpoint, or --resume
158
+ const useDebug = options.debug || options.checkpoint || options.resume;
159
+ let debugController;
160
+ if (useDebug) {
161
+ // Get execution order for the controller
162
+ let executionOrder = resumeExecutionOrder;
163
+ if (!executionOrder) {
164
+ const source = fs.readFileSync(filePath, 'utf8');
165
+ const parsed = await parseWorkflow(source, { workflowName: options.workflow });
166
+ if (parsed.errors.length === 0) {
167
+ executionOrder = getTopologicalOrder(parsed.ast);
168
+ }
169
+ else {
170
+ executionOrder = [];
171
+ }
172
+ }
173
+ // Set up checkpoint writer
174
+ let checkpointWriter;
175
+ if (options.checkpoint || options.resume) {
176
+ const runId = `run-${Date.now()}-${Math.random().toString(36).slice(2)}`;
177
+ checkpointWriter = new CheckpointWriter(filePath, resumeWorkflowName ?? options.workflow ?? 'default', runId, params);
178
+ }
179
+ debugController = new DebugController({
180
+ debug: options.debug ?? false,
181
+ checkpoint: !!(options.checkpoint || options.resume),
182
+ checkpointWriter,
183
+ breakpoints: options.breakpoint,
184
+ executionOrder,
185
+ skipNodes: resumeSkipNodes,
186
+ });
187
+ }
114
188
  // Build onEvent callback for real-time streaming
115
189
  const nodeStartTimes = new Map();
116
190
  const onEvent = options.stream && !options.json
@@ -141,25 +215,58 @@ export async function runCommand(input, options) {
141
215
  : undefined;
142
216
  const channel = new AgentChannel();
143
217
  const execPromise = executeWorkflowFromFile(filePath, params, {
144
- workflowName: options.workflow,
218
+ workflowName: resumeWorkflowName ?? options.workflow,
145
219
  production: options.production ?? false,
146
220
  includeTrace,
147
221
  mocks,
148
222
  agentChannel: channel,
223
+ debugController,
149
224
  onEvent,
150
225
  });
226
+ // If debug mode is active and interactive, enter the debug REPL
227
+ if (options.debug && debugController && process.stdin.isTTY) {
228
+ const debugResult = await runDebugRepl(debugController, execPromise, channel, options);
229
+ if (timedOut)
230
+ return;
231
+ if (options.json) {
232
+ process.stdout.write(JSON.stringify({
233
+ success: true,
234
+ result: debugResult,
235
+ ...(resumeCheckpointPath && { resumedFrom: resumeCheckpointPath }),
236
+ ...(resumeRerunNodes && resumeRerunNodes.length > 0 && { rerunNodes: resumeRerunNodes }),
237
+ ...(resumeStale && { warning: 'Workflow changed since checkpoint.' }),
238
+ }, null, 2) + '\n');
239
+ }
240
+ else {
241
+ logger.success('Debug session completed');
242
+ logger.newline();
243
+ logger.section('Result');
244
+ logger.log(JSON.stringify(debugResult, null, 2));
245
+ }
246
+ return;
247
+ }
151
248
  let result;
152
249
  let execDone = false;
153
250
  // Race loop: detect pauses, prompt user, resume
154
251
  while (!execDone) {
155
- const raceResult = await Promise.race([
252
+ const promises = [
156
253
  execPromise.then((r) => ({ type: 'completed', result: r })),
157
- channel.onPause().then((req) => ({ type: 'paused', request: req })),
158
- ]);
254
+ channel.onPause().then((req) => ({ type: 'agent_paused', request: req })),
255
+ ];
256
+ // Also race against debug controller pause if present (non-interactive checkpoint mode)
257
+ if (debugController) {
258
+ promises.push(debugController.onPause().then((state) => ({ type: 'debug_paused', state })));
259
+ }
260
+ const raceResult = await Promise.race(promises);
159
261
  if (raceResult.type === 'completed') {
160
262
  result = raceResult.result;
161
263
  execDone = true;
162
264
  }
265
+ else if (raceResult.type === 'debug_paused') {
266
+ // Non-interactive debug mode (checkpoint only, no --debug flag):
267
+ // Auto-continue, the checkpoint was written in afterNode
268
+ debugController.resume({ type: 'continue' });
269
+ }
163
270
  else {
164
271
  // Workflow paused at waitForAgent
165
272
  const request = raceResult.request;
@@ -200,6 +307,8 @@ export async function runCommand(input, options) {
200
307
  executionTime: result.executionTime,
201
308
  result: result.result,
202
309
  ...(includeTrace && result.trace && { traceCount: result.trace.length }),
310
+ ...(resumeCheckpointPath && { resumedFrom: resumeCheckpointPath }),
311
+ ...(resumeRerunNodes && resumeRerunNodes.length > 0 && { rerunNodes: resumeRerunNodes }),
203
312
  }, null, 2) + '\n');
204
313
  }
205
314
  else {
@@ -301,6 +410,289 @@ export async function validateMockConfig(mocks, filePath, workflowName) {
301
410
  // Parsing failed — skip validation, the execution will report the real error
302
411
  }
303
412
  }
413
+ // ---------------------------------------------------------------------------
414
+ // Interactive debug REPL
415
+ // ---------------------------------------------------------------------------
416
+ function printDebugState(state) {
417
+ const pos = `${state.position + 1}/${state.executionOrder.length}`;
418
+ logger.log(`\n[paused] ${state.phase}: ${state.currentNodeId} (${pos})`);
419
+ if (state.phase === 'after' && state.currentNodeOutputs) {
420
+ // Show outputs of the node that just completed
421
+ for (const [port, value] of Object.entries(state.currentNodeOutputs)) {
422
+ const valueStr = JSON.stringify(value);
423
+ const display = valueStr.length > 80 ? valueStr.substring(0, 77) + '...' : valueStr;
424
+ logger.log(` ${state.currentNodeId}.${port} = ${display}`);
425
+ }
426
+ }
427
+ }
428
+ function printDebugHelp() {
429
+ logger.log('Commands:');
430
+ logger.log(' s, step Step to next node');
431
+ logger.log(' c, continue Run to completion');
432
+ logger.log(' cb Continue to next breakpoint');
433
+ logger.log(' i, inspect Show all variables');
434
+ logger.log(' i <node> Show variables for a specific node');
435
+ logger.log(' b <node> Add breakpoint');
436
+ logger.log(' rb <node> Remove breakpoint');
437
+ logger.log(' bl List breakpoints');
438
+ logger.log(' set <node>.<port> <json> Modify a variable');
439
+ logger.log(' q, quit Abort debug session');
440
+ logger.log(' h, help Show this help');
441
+ }
442
+ async function runDebugRepl(controller, execPromise, agentChannel, options) {
443
+ if (!options.json) {
444
+ logger.newline();
445
+ logger.section('Flow Weaver Debug');
446
+ logger.log('Type "h" for help.');
447
+ }
448
+ // Wait for the first pause
449
+ const firstResult = await Promise.race([
450
+ execPromise.then((r) => ({ type: 'completed', result: r })),
451
+ controller.onPause().then((state) => ({ type: 'paused', state })),
452
+ ]);
453
+ if (firstResult.type === 'completed') {
454
+ return firstResult.result.result;
455
+ }
456
+ let currentState = firstResult.state;
457
+ printDebugState(currentState);
458
+ const rl = readline.createInterface({
459
+ input: process.stdin,
460
+ output: process.stderr,
461
+ prompt: '> ',
462
+ });
463
+ return new Promise((resolve, reject) => {
464
+ let resolved = false;
465
+ function finish(value) {
466
+ if (resolved)
467
+ return;
468
+ resolved = true;
469
+ rl.close();
470
+ resolve(value);
471
+ }
472
+ function fail(err) {
473
+ if (resolved)
474
+ return;
475
+ resolved = true;
476
+ rl.close();
477
+ reject(err);
478
+ }
479
+ async function handleResume() {
480
+ const raceResult = await Promise.race([
481
+ execPromise.then((r) => ({ type: 'completed', result: r })),
482
+ controller.onPause().then((state) => ({ type: 'paused', state })),
483
+ agentChannel.onPause().then((req) => ({ type: 'agent_paused', request: req })),
484
+ ]);
485
+ if (raceResult.type === 'completed') {
486
+ const execResult = raceResult.result;
487
+ if (!options.json) {
488
+ logger.success(`\nWorkflow completed in ${execResult.executionTime}ms`);
489
+ }
490
+ finish(execResult.result);
491
+ }
492
+ else if (raceResult.type === 'paused') {
493
+ currentState = raceResult.state;
494
+ printDebugState(currentState);
495
+ rl.prompt();
496
+ }
497
+ else {
498
+ // Agent pause during debug: prompt user for agent input
499
+ const request = raceResult.request;
500
+ const label = request.prompt || `Agent "${request.agentId}" is requesting input`;
501
+ logger.log(`\n[waitForAgent] ${label}`);
502
+ rl.question('Agent response (JSON): ', (answer) => {
503
+ let parsed;
504
+ try {
505
+ parsed = JSON.parse(answer);
506
+ }
507
+ catch {
508
+ parsed = { response: answer };
509
+ }
510
+ agentChannel.resume(parsed);
511
+ // Re-race after agent resume
512
+ handleResume().catch(fail);
513
+ });
514
+ }
515
+ }
516
+ rl.on('line', async (line) => {
517
+ const input = line.trim();
518
+ if (!input) {
519
+ rl.prompt();
520
+ return;
521
+ }
522
+ const parts = input.split(/\s+/);
523
+ const cmd = parts[0].toLowerCase();
524
+ try {
525
+ switch (cmd) {
526
+ case 's':
527
+ case 'step':
528
+ controller.resume({ type: 'step' });
529
+ await handleResume();
530
+ break;
531
+ case 'c':
532
+ case 'continue':
533
+ controller.resume({ type: 'continue' });
534
+ await handleResume();
535
+ break;
536
+ case 'cb':
537
+ controller.resume({ type: 'continueToBreakpoint' });
538
+ await handleResume();
539
+ break;
540
+ case 'i':
541
+ case 'inspect': {
542
+ const nodeId = parts[1];
543
+ if (nodeId) {
544
+ const prefix = `${nodeId}:`;
545
+ let found = false;
546
+ for (const [key, value] of Object.entries(currentState.variables)) {
547
+ if (key.startsWith(prefix)) {
548
+ found = true;
549
+ const portKey = key.substring(prefix.length);
550
+ logger.log(` ${nodeId}.${portKey} = ${JSON.stringify(value)}`);
551
+ }
552
+ }
553
+ if (!found) {
554
+ logger.log(` No variables found for node "${nodeId}"`);
555
+ }
556
+ }
557
+ else {
558
+ // Group by node
559
+ const byNode = new Map();
560
+ for (const [key, value] of Object.entries(currentState.variables)) {
561
+ const firstColon = key.indexOf(':');
562
+ if (firstColon === -1)
563
+ continue;
564
+ const node = key.substring(0, firstColon);
565
+ if (!byNode.has(node))
566
+ byNode.set(node, {});
567
+ byNode.get(node)[key.substring(firstColon + 1)] = value;
568
+ }
569
+ for (const [node, vars] of byNode) {
570
+ logger.log(` ${node}:`);
571
+ for (const [port, value] of Object.entries(vars)) {
572
+ const valueStr = JSON.stringify(value);
573
+ const display = valueStr.length > 60 ? valueStr.substring(0, 57) + '...' : valueStr;
574
+ logger.log(` ${port} = ${display}`);
575
+ }
576
+ }
577
+ }
578
+ rl.prompt();
579
+ break;
580
+ }
581
+ case 'b': {
582
+ const nodeId = parts[1];
583
+ if (!nodeId) {
584
+ logger.log('Usage: b <nodeId>');
585
+ }
586
+ else {
587
+ controller.addBreakpoint(nodeId);
588
+ logger.log(`Breakpoint added: ${nodeId}`);
589
+ }
590
+ rl.prompt();
591
+ break;
592
+ }
593
+ case 'rb': {
594
+ const nodeId = parts[1];
595
+ if (!nodeId) {
596
+ logger.log('Usage: rb <nodeId>');
597
+ }
598
+ else {
599
+ controller.removeBreakpoint(nodeId);
600
+ logger.log(`Breakpoint removed: ${nodeId}`);
601
+ }
602
+ rl.prompt();
603
+ break;
604
+ }
605
+ case 'bl':
606
+ logger.log(`Breakpoints: ${controller.getBreakpoints().join(', ') || '(none)'}`);
607
+ rl.prompt();
608
+ break;
609
+ case 'set': {
610
+ // set node.port <json_value>
611
+ const target = parts[1];
612
+ const jsonValue = parts.slice(2).join(' ');
613
+ if (!target || !jsonValue) {
614
+ logger.log('Usage: set <node>.<port> <json_value>');
615
+ rl.prompt();
616
+ break;
617
+ }
618
+ const dotIdx = target.indexOf('.');
619
+ if (dotIdx === -1) {
620
+ logger.log('Target must be in format: node.port');
621
+ rl.prompt();
622
+ break;
623
+ }
624
+ const nodeId = target.substring(0, dotIdx);
625
+ const portName = target.substring(dotIdx + 1);
626
+ let value;
627
+ try {
628
+ value = JSON.parse(jsonValue);
629
+ }
630
+ catch {
631
+ logger.log(`Invalid JSON value: ${jsonValue}`);
632
+ rl.prompt();
633
+ break;
634
+ }
635
+ // Find the key in current variables
636
+ const prefix = `${nodeId}:${portName}:`;
637
+ let foundKey = null;
638
+ let latestIdx = -1;
639
+ for (const key of Object.keys(currentState.variables)) {
640
+ if (key.startsWith(prefix)) {
641
+ const idx = parseInt(key.substring(prefix.length), 10);
642
+ if (idx > latestIdx) {
643
+ latestIdx = idx;
644
+ foundKey = key;
645
+ }
646
+ }
647
+ }
648
+ if (!foundKey) {
649
+ logger.log(`Variable not found: ${nodeId}.${portName}`);
650
+ rl.prompt();
651
+ break;
652
+ }
653
+ controller.setVariable(foundKey, value);
654
+ currentState.variables[foundKey] = value;
655
+ logger.log(`Set ${nodeId}.${portName} = ${JSON.stringify(value)}`);
656
+ rl.prompt();
657
+ break;
658
+ }
659
+ case 'q':
660
+ case 'quit':
661
+ controller.resume({ type: 'abort' });
662
+ finish(undefined);
663
+ break;
664
+ case 'h':
665
+ case 'help':
666
+ printDebugHelp();
667
+ rl.prompt();
668
+ break;
669
+ default:
670
+ logger.log(`Unknown command: ${cmd}. Type "h" for help.`);
671
+ rl.prompt();
672
+ }
673
+ }
674
+ catch (err) {
675
+ if (!resolved) {
676
+ const msg = err instanceof Error ? err.message : String(err);
677
+ if (msg.includes('aborted')) {
678
+ logger.log('Debug session aborted.');
679
+ finish(undefined);
680
+ }
681
+ else {
682
+ logger.error(`Error: ${msg}`);
683
+ rl.prompt();
684
+ }
685
+ }
686
+ }
687
+ });
688
+ rl.on('close', () => {
689
+ if (!resolved) {
690
+ finish(undefined);
691
+ }
692
+ });
693
+ rl.prompt();
694
+ });
695
+ }
304
696
  function promptForInput(question) {
305
697
  return new Promise((resolve) => {
306
698
  const rl = readline.createInterface({
@@ -1,20 +1,19 @@
1
1
  /**
2
- * tunnel command — Relay Studio RPC calls from cloud to a local dev server.
2
+ * tunnel command — Self-contained tunnel for cloud Studio.
3
3
  *
4
- * 1. Connects to the local server via Socket.IO (same as listen command).
5
- * 2. Opens a WebSocket to the cloud server's /api/tunnel endpoint.
6
- * 3. Relays tunnel:request messages from cloud → local Socket.IO → cloud.
4
+ * Handles all RPC methods locally using Node.js fs and the
5
+ * @synergenius/flow-weaver AST API. No local server required.
6
+ *
7
+ * 1. Opens a WebSocket to the cloud server's /api/tunnel endpoint.
8
+ * 2. Dispatches RPC calls directly to local handler functions.
7
9
  */
8
- import { io as socketIO } from 'socket.io-client';
9
10
  import WebSocket from 'ws';
10
11
  export interface TunnelOptions {
11
12
  key: string;
12
13
  cloud?: string;
13
- server?: string;
14
+ dir?: string;
14
15
  /** Override WebSocket factory (for testing) */
15
16
  createWs?: (url: string) => WebSocket;
16
- /** Override socket.io-client factory (for testing) */
17
- ioFactory?: typeof socketIO;
18
17
  }
19
18
  export declare function tunnelCommand(options: TunnelOptions): Promise<void>;
20
19
  //# sourceMappingURL=tunnel.d.ts.map