@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.
- package/dist/api/generate-in-place.js +5 -4
- package/dist/api/inline-runtime.js +42 -0
- package/dist/cli/commands/run.d.ts +8 -0
- package/dist/cli/commands/run.js +396 -4
- package/dist/cli/commands/tunnel.d.ts +7 -8
- package/dist/cli/commands/tunnel.js +27 -64
- package/dist/cli/flow-weaver.mjs +59844 -54301
- package/dist/cli/index.js +6 -2
- package/dist/cli/tunnel/dispatch.d.ts +18 -0
- package/dist/cli/tunnel/dispatch.js +36 -0
- package/dist/cli/tunnel/file-lock.d.ts +9 -0
- package/dist/cli/tunnel/file-lock.js +36 -0
- package/dist/cli/tunnel/handlers/ast-ops.d.ts +10 -0
- package/dist/cli/tunnel/handlers/ast-ops.js +252 -0
- package/dist/cli/tunnel/handlers/execution.d.ts +7 -0
- package/dist/cli/tunnel/handlers/execution.js +89 -0
- package/dist/cli/tunnel/handlers/file-ops.d.ts +7 -0
- package/dist/cli/tunnel/handlers/file-ops.js +204 -0
- package/dist/cli/tunnel/handlers/mutations.d.ts +7 -0
- package/dist/cli/tunnel/handlers/mutations.js +285 -0
- package/dist/cli/tunnel/handlers/stubs.d.ts +7 -0
- package/dist/cli/tunnel/handlers/stubs.js +141 -0
- package/dist/cli/tunnel/handlers/templates.d.ts +7 -0
- package/dist/cli/tunnel/handlers/templates.js +123 -0
- package/dist/cli/tunnel/path-resolver.d.ts +17 -0
- package/dist/cli/tunnel/path-resolver.js +52 -0
- package/dist/doc-metadata/extractors/mcp-tools.js +189 -0
- package/dist/doc-metadata/types.d.ts +1 -1
- package/dist/generator/unified.js +112 -35
- package/dist/mcp/debug-session.d.ts +30 -0
- package/dist/mcp/debug-session.js +25 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/server.js +2 -0
- package/dist/mcp/tools-debug.d.ts +3 -0
- package/dist/mcp/tools-debug.js +451 -0
- package/dist/mcp/workflow-executor.d.ts +2 -0
- package/dist/mcp/workflow-executor.js +12 -2
- package/dist/runtime/ExecutionContext.d.ts +19 -0
- package/dist/runtime/ExecutionContext.js +43 -0
- package/dist/runtime/checkpoint.d.ts +84 -0
- package/dist/runtime/checkpoint.js +225 -0
- package/dist/runtime/debug-controller.d.ts +110 -0
- package/dist/runtime/debug-controller.js +247 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +2 -0
- package/docs/reference/cli-reference.md +9 -1
- package/docs/reference/debugging.md +152 -5
- 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
|
|
118
|
-
|
|
119
|
-
|
|
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,
|
|
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.
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -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
|
|
252
|
+
const promises = [
|
|
156
253
|
execPromise.then((r) => ({ type: 'completed', result: r })),
|
|
157
|
-
channel.onPause().then((req) => ({ type: '
|
|
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 —
|
|
2
|
+
* tunnel command — Self-contained tunnel for cloud Studio.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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
|