@runflow-ai/cli 0.2.7 → 0.2.9

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.
@@ -11,35 +11,43 @@ app.use(express.json());
11
11
  // Configuração será injetada pela CLI
12
12
  const CONFIG = {
13
13
  port: process.env.RUNFLOW_PORT || 8547,
14
- mainFile: process.env.RUNFLOW_MAIN_FILE || 'main.ts'
14
+ mainFile: process.env.RUNFLOW_MAIN_FILE || 'main.ts',
15
+ verbose: process.env.RUNFLOW_VERBOSE === 'true',
15
16
  };
16
17
 
18
+ // Log de inicialização
19
+ console.log('🚀 [INIT] Test server starting...');
20
+ console.log('🔧 [CONFIG]', CONFIG);
21
+ if (CONFIG.verbose) {
22
+ console.log('📝 [VERBOSE] Verbose mode is ENABLED');
23
+ }
24
+
17
25
  // ============================================================================
18
26
  // Load rf.json (agent configuration)
19
27
  // ============================================================================
20
28
 
21
29
  function loadRfConfig() {
22
30
  const rfConfigPath = path.join(process.cwd(), '.runflow', 'rf.json');
23
-
31
+
24
32
  if (fs.existsSync(rfConfigPath)) {
25
33
  try {
26
34
  const config = JSON.parse(fs.readFileSync(rfConfigPath, 'utf8'));
27
35
  console.log('✅ Loaded rf.json:', {
28
36
  agentId: config.agentId,
29
37
  tenantId: config.tenantId,
30
- apiUrl: config.apiUrl
38
+ apiUrl: config.apiUrl,
31
39
  });
32
40
  return config;
33
41
  } catch (error) {
34
42
  console.warn('⚠️ Could not parse rf.json:', error.message);
35
43
  }
36
44
  }
37
-
45
+
38
46
  console.warn('⚠️ No rf.json found - using defaults');
39
47
  return {
40
48
  agentId: 'local-agent',
41
49
  tenantId: 'test-local',
42
- apiUrl: 'http://localhost:3001'
50
+ apiUrl: 'http://localhost:3001',
43
51
  };
44
52
  }
45
53
 
@@ -59,34 +67,34 @@ function generateRandomId() {
59
67
 
60
68
  function generateThreadId(input) {
61
69
  const companyId = input.companyId || 'test';
62
-
70
+
63
71
  // 1. Explicit
64
72
  if (input.entityType && input.entityValue) {
65
73
  const cleanValue = String(input.entityValue).replace(/[^a-zA-Z0-9]/g, '_');
66
74
  return `${input.entityType}_${companyId}_${cleanValue}`;
67
75
  }
68
-
76
+
69
77
  // 2. SessionId
70
78
  if (input.sessionId) {
71
79
  return `session_${companyId}_${input.sessionId}`;
72
80
  }
73
-
81
+
74
82
  // 3. Phone (WhatsApp/Twilio)
75
83
  if (input.metadata?.phone || input.metadata?.From) {
76
84
  const phone = (input.metadata.phone || input.metadata.From).replace(/\D/g, '');
77
85
  return `phone_${companyId}_${phone}`;
78
86
  }
79
-
87
+
80
88
  // 4. Email
81
89
  if (input.metadata?.email) {
82
90
  return `email_${companyId}_${input.metadata.email}`;
83
91
  }
84
-
92
+
85
93
  // 5. ContactId (HubSpot)
86
94
  if (input.metadata?.contactId) {
87
95
  return `hubspot_contact_${companyId}_${input.metadata.contactId}`;
88
96
  }
89
-
97
+
90
98
  // 6. Fallback
91
99
  return `thread_${companyId}_${Date.now()}_${generateRandomId()}`;
92
100
  }
@@ -130,11 +138,11 @@ app.post('/api/chat', async (req, res) => {
130
138
  try {
131
139
  console.log('\n📥 [Test] Received:', req.body.message);
132
140
  console.log('🔄 [Reload] Loading fresh code from', CONFIG.mainFile + '...');
133
-
141
+
134
142
  // ============================================================================
135
143
  // Generate Execution Context (simula Execution Engine)
136
144
  // ============================================================================
137
-
145
+
138
146
  const input = {
139
147
  message: req.body.message,
140
148
  sessionId: req.body.sessionId || `test_session_${Date.now()}`,
@@ -142,15 +150,15 @@ app.post('/api/chat', async (req, res) => {
142
150
  userId: req.body.userId,
143
151
  channel: req.body.channel || 'test',
144
152
  metadata: req.body.metadata || {},
145
- timestamp: new Date().toISOString()
153
+ timestamp: new Date().toISOString(),
146
154
  };
147
-
155
+
148
156
  const executionId = generateExecutionId();
149
157
  const threadId = generateThreadId(input);
150
158
  const entityType = inferEntityType(input);
151
159
  const entityValue = inferEntityValue(input);
152
160
  const userId = inferUserId(input);
153
-
161
+
154
162
  console.log('🔍 [Context Generated]:', {
155
163
  executionId,
156
164
  threadId,
@@ -158,12 +166,25 @@ app.post('/api/chat', async (req, res) => {
158
166
  entityValue: entityValue || '(none)',
159
167
  userId: userId || '(none)',
160
168
  });
161
-
169
+
162
170
  // Executar TypeScript diretamente com tsx
163
171
  const inputData = JSON.stringify(input);
164
172
 
165
- const tsxProcess = spawn('npx', ['tsx', '--eval', `
166
- import { main } from './${CONFIG.mainFile}';
173
+ if (CONFIG.verbose) {
174
+ console.log('\n📝 [Verbose] Executing:', CONFIG.mainFile);
175
+ console.log('📝 [Verbose] Input:', input);
176
+ console.log('📝 [Verbose] Working Directory:', process.cwd());
177
+ }
178
+
179
+ // Criar arquivo temporário para execução (mais confiável que --eval)
180
+ const tempDir = path.join(process.cwd(), '.runflow');
181
+ if (!fs.existsSync(tempDir)) {
182
+ fs.mkdirSync(tempDir, { recursive: true });
183
+ }
184
+
185
+ const tempFile = path.join(tempDir, `exec_${Date.now()}.ts`);
186
+ const tsxCode = `
187
+ import { main } from '../${CONFIG.mainFile}';
167
188
 
168
189
  const input = ${inputData};
169
190
 
@@ -174,11 +195,20 @@ main(input).then(result => {
174
195
  process.exit(0);
175
196
  }).catch(error => {
176
197
  console.log('🔴 RUNFLOW_ERROR_START');
177
- console.log(JSON.stringify({ error: error.message }));
198
+ console.log(JSON.stringify({ error: error.message, stack: error.stack }));
178
199
  console.log('🔴 RUNFLOW_ERROR_END');
179
200
  process.exit(1);
180
201
  });
181
- `], {
202
+ `;
203
+
204
+ fs.writeFileSync(tempFile, tsxCode);
205
+
206
+ if (CONFIG.verbose) {
207
+ console.log('\n📜 [TSX Code written to]:', tempFile);
208
+ }
209
+
210
+ const tsxProcess = spawn('npx', ['tsx', tempFile], {
211
+ cwd: process.cwd(), // Executar no diretório do projeto
182
212
  env: {
183
213
  ...process.env,
184
214
  // ✅ Passar context via env (igual Execution Engine)
@@ -191,9 +221,29 @@ main(input).then(result => {
191
221
  RUNFLOW_LOCAL_TRACES: 'true',
192
222
  RUNFLOW_ENV: 'development',
193
223
  },
194
- stdio: ['pipe', 'pipe', 'pipe']
224
+ stdio: ['pipe', 'pipe', 'pipe'], // Sempre pipe para capturar resultado
225
+ // No Windows, npx precisa de shell: true
226
+ shell: process.platform === 'win32',
195
227
  });
196
-
228
+
229
+ if (CONFIG.verbose) {
230
+ console.log('🚀 [Process] tsx spawned with PID:', tsxProcess.pid);
231
+ }
232
+
233
+ // Cleanup: remover arquivo temporário após execução
234
+ const cleanupTempFile = () => {
235
+ try {
236
+ if (fs.existsSync(tempFile)) {
237
+ fs.unlinkSync(tempFile);
238
+ if (CONFIG.verbose) {
239
+ console.log('🗑️ [Cleanup] Temp file removed');
240
+ }
241
+ }
242
+ } catch (e) {
243
+ // Ignorar erros de cleanup
244
+ }
245
+ };
246
+
197
247
  let output = '';
198
248
  let errorOutput = '';
199
249
  let hasResponded = false;
@@ -201,7 +251,7 @@ main(input).then(result => {
201
251
  let isCapturingError = false;
202
252
  let resultLines = [];
203
253
  let errorLines = [];
204
-
254
+
205
255
  // Processar saída linha por linha
206
256
  const processLine = (line) => {
207
257
  if (line.includes('🔵 RUNFLOW_RESULT_START')) {
@@ -209,15 +259,15 @@ main(input).then(result => {
209
259
  resultLines = [];
210
260
  return;
211
261
  }
212
-
262
+
213
263
  if (line.includes('🔵 RUNFLOW_RESULT_END')) {
214
264
  isCapturingResult = false;
215
-
265
+
216
266
  if (!hasResponded && resultLines.length > 0) {
217
267
  try {
218
268
  const jsonResult = JSON.parse(resultLines.join('\n'));
219
269
  console.log('✅ [Result Captured]:', jsonResult);
220
-
270
+
221
271
  res.json({
222
272
  success: true,
223
273
  data: jsonResult,
@@ -229,7 +279,7 @@ main(input).then(result => {
229
279
  entityValue,
230
280
  userId,
231
281
  },
232
- timestamp: new Date().toISOString()
282
+ timestamp: new Date().toISOString(),
233
283
  });
234
284
  hasResponded = true;
235
285
  console.log('✅ [Response] Sent to frontend successfully');
@@ -239,25 +289,25 @@ main(input).then(result => {
239
289
  }
240
290
  return;
241
291
  }
242
-
292
+
243
293
  if (line.includes('🔴 RUNFLOW_ERROR_START')) {
244
294
  isCapturingError = true;
245
295
  errorLines = [];
246
296
  return;
247
297
  }
248
-
298
+
249
299
  if (line.includes('🔴 RUNFLOW_ERROR_END')) {
250
300
  isCapturingError = false;
251
-
301
+
252
302
  if (!hasResponded && errorLines.length > 0) {
253
303
  try {
254
304
  const errorResult = JSON.parse(errorLines.join('\n'));
255
305
  console.log('❌ [Error Captured]:', errorResult);
256
-
306
+
257
307
  res.status(500).json({
258
308
  success: false,
259
309
  error: errorResult.error,
260
- timestamp: new Date().toISOString()
310
+ timestamp: new Date().toISOString(),
261
311
  });
262
312
  hasResponded = true;
263
313
  } catch (e) {
@@ -266,7 +316,7 @@ main(input).then(result => {
266
316
  }
267
317
  return;
268
318
  }
269
-
319
+
270
320
  if (isCapturingResult) {
271
321
  resultLines.push(line);
272
322
  } else if (isCapturingError) {
@@ -274,35 +324,58 @@ main(input).then(result => {
274
324
  }
275
325
  };
276
326
 
277
- tsxProcess.stdout.on('data', (data) => {
327
+ // Sempre capturar output, mas mostrar logs detalhados se verbose
328
+ tsxProcess.stdout?.on('data', (data) => {
278
329
  const chunk = data.toString();
279
- console.log('📤 [stdout]:', chunk.trim());
330
+ if (CONFIG.verbose) {
331
+ console.log('📤 [TSX stdout]:', chunk.trim());
332
+ }
280
333
  output += chunk;
281
-
334
+
282
335
  const lines = chunk.split('\n');
283
- lines.forEach(line => {
336
+ lines.forEach((line) => {
284
337
  if (line.trim()) {
285
338
  processLine(line.trim());
286
339
  }
287
340
  });
288
341
  });
289
-
290
- tsxProcess.stderr.on('data', (data) => {
342
+
343
+ tsxProcess.stderr?.on('data', (data) => {
291
344
  const chunk = data.toString();
292
- console.log('📤 [stderr]:', chunk.trim());
345
+ if (CONFIG.verbose) {
346
+ console.log('📤 [TSX stderr]:', chunk.trim());
347
+ }
293
348
  errorOutput += chunk;
294
349
  });
295
350
 
351
+ tsxProcess.on('error', (error) => {
352
+ console.error('❌ [Process Error]:', error.message);
353
+ if (!hasResponded) {
354
+ res.status(500).json({
355
+ success: false,
356
+ error: `Failed to execute: ${error.message}`,
357
+ timestamp: new Date().toISOString(),
358
+ });
359
+ hasResponded = true;
360
+ }
361
+ });
362
+
296
363
  tsxProcess.on('close', (code) => {
297
- console.log('🔚 [Process] Closed with code:', code);
298
-
364
+ cleanupTempFile(); // Limpar arquivo temporário
365
+
366
+ if (CONFIG.verbose) {
367
+ console.log('🔚 [Process] Closed with code:', code);
368
+ console.log('📊 [Captured Output]:', output.substring(0, 500)); // Primeiros 500 chars
369
+ console.log('📊 [Captured Error]:', errorOutput.substring(0, 500));
370
+ }
371
+
299
372
  if (!hasResponded) {
300
373
  console.log('⚠️ [Fallback] No structured response captured');
301
-
374
+
302
375
  if (code === 0) {
303
376
  const lines = output.trim().split('\n');
304
377
  let jsonResult = null;
305
-
378
+
306
379
  for (let i = lines.length - 1; i >= 0; i--) {
307
380
  const line = lines[i].trim();
308
381
  if (line.startsWith('{') && line.endsWith('}')) {
@@ -315,48 +388,47 @@ main(input).then(result => {
315
388
  }
316
389
  }
317
390
  }
318
-
391
+
319
392
  if (jsonResult) {
320
393
  res.json({
321
394
  success: true,
322
395
  data: jsonResult,
323
- timestamp: new Date().toISOString()
396
+ timestamp: new Date().toISOString(),
324
397
  });
325
398
  } else {
326
399
  res.json({
327
400
  success: true,
328
401
  data: { message: 'Process completed but no result captured' },
329
- timestamp: new Date().toISOString()
402
+ timestamp: new Date().toISOString(),
330
403
  });
331
404
  }
332
405
  } else {
333
406
  res.status(500).json({
334
407
  success: false,
335
408
  error: errorOutput || `Process failed with code ${code}`,
336
- timestamp: new Date().toISOString()
409
+ timestamp: new Date().toISOString(),
337
410
  });
338
411
  }
339
412
  }
340
413
  });
341
-
342
414
  } catch (error) {
343
415
  console.error('❌ [Test] Error:', error.message);
344
-
416
+
345
417
  res.status(500).json({
346
418
  success: false,
347
419
  error: error.message,
348
- timestamp: new Date().toISOString()
420
+ timestamp: new Date().toISOString(),
349
421
  });
350
422
  }
351
423
  });
352
424
 
353
425
  // Health check
354
426
  app.get('/api/health', (req, res) => {
355
- res.json({
356
- status: 'ok',
427
+ res.json({
428
+ status: 'ok',
357
429
  timestamp: new Date().toISOString(),
358
430
  agent: CONFIG.mainFile,
359
- type: 'test-server'
431
+ type: 'test-servers',
360
432
  });
361
433
  });
362
434
 
@@ -364,17 +436,17 @@ app.get('/api/health', (req, res) => {
364
436
  app.get('/api/scenarios', (req, res) => {
365
437
  const fs = require('fs');
366
438
  const path = require('path');
367
-
439
+
368
440
  const scenarios = [];
369
-
441
+
370
442
  // 1. Load CLI default scenarios
371
443
  try {
372
444
  const cliScenariosPath = process.env.RUNFLOW_SCENARIOS_PATH;
373
-
445
+
374
446
  if (cliScenariosPath && fs.existsSync(cliScenariosPath)) {
375
- const files = fs.readdirSync(cliScenariosPath).filter(f => f.endsWith('.json'));
376
-
377
- files.forEach(file => {
447
+ const files = fs.readdirSync(cliScenariosPath).filter((f) => f.endsWith('.json'));
448
+
449
+ files.forEach((file) => {
378
450
  try {
379
451
  const content = JSON.parse(fs.readFileSync(path.join(cliScenariosPath, file), 'utf8'));
380
452
  scenarios.push({ ...content, source: 'cli', id: file.replace('.json', '') });
@@ -386,15 +458,17 @@ app.get('/api/scenarios', (req, res) => {
386
458
  } catch (error) {
387
459
  console.error('Error loading CLI scenarios:', error.message);
388
460
  }
389
-
461
+
390
462
  // 2. Load project custom scenarios
391
463
  try {
392
464
  const projectScenariosPath = path.join(process.cwd(), 'test-scenarios');
393
465
  if (fs.existsSync(projectScenariosPath)) {
394
- const files = fs.readdirSync(projectScenariosPath).filter(f => f.endsWith('.json'));
395
- files.forEach(file => {
466
+ const files = fs.readdirSync(projectScenariosPath).filter((f) => f.endsWith('.json'));
467
+ files.forEach((file) => {
396
468
  try {
397
- const content = JSON.parse(fs.readFileSync(path.join(projectScenariosPath, file), 'utf8'));
469
+ const content = JSON.parse(
470
+ fs.readFileSync(path.join(projectScenariosPath, file), 'utf8'),
471
+ );
398
472
  scenarios.push({ ...content, source: 'project', id: file.replace('.json', '') });
399
473
  } catch (e) {
400
474
  console.error('Error loading project scenario ' + file + ':', e.message);
@@ -404,7 +478,7 @@ app.get('/api/scenarios', (req, res) => {
404
478
  } catch (error) {
405
479
  console.error('Error loading project scenarios:', error.message);
406
480
  }
407
-
481
+
408
482
  res.json({ scenarios });
409
483
  });
410
484
 
@@ -419,22 +493,22 @@ app.get('/api/scenarios', (req, res) => {
419
493
  app.get('/api/traces', (req, res) => {
420
494
  try {
421
495
  const tracesFile = path.join(process.cwd(), '.runflow', 'traces.json');
422
-
496
+
423
497
  if (!fs.existsSync(tracesFile)) {
424
- return res.json({
425
- threads: [],
498
+ return res.json({
499
+ threads: [],
426
500
  total: 0,
427
- message: 'No traces yet. Run your agent to generate traces.'
501
+ message: 'No traces yet. Run your agent to generate traces.',
428
502
  });
429
503
  }
430
-
504
+
431
505
  const data = JSON.parse(fs.readFileSync(tracesFile, 'utf-8'));
432
-
506
+
433
507
  // Group executions by threadId
434
508
  const threadMap = {};
435
- Object.values(data.executions || {}).forEach(execution => {
509
+ Object.values(data.executions || {}).forEach((execution) => {
436
510
  const threadId = execution.threadId || 'unknown';
437
-
511
+
438
512
  if (!threadMap[threadId]) {
439
513
  threadMap[threadId] = {
440
514
  threadId,
@@ -445,23 +519,24 @@ app.get('/api/traces', (req, res) => {
445
519
  lastActivity: execution.startedAt,
446
520
  };
447
521
  }
448
-
522
+
449
523
  threadMap[threadId].executions.push(execution);
450
524
  threadMap[threadId].totalTraces += execution.traces?.length || 0;
451
-
525
+
452
526
  // Update last activity
453
527
  if (execution.startedAt > threadMap[threadId].lastActivity) {
454
528
  threadMap[threadId].lastActivity = execution.startedAt;
455
529
  }
456
530
  });
457
-
458
- const threads = Object.values(threadMap)
459
- .sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
460
-
461
- res.json({
531
+
532
+ const threads = Object.values(threadMap).sort(
533
+ (a, b) => new Date(b.lastActivity) - new Date(a.lastActivity),
534
+ );
535
+
536
+ res.json({
462
537
  threads,
463
538
  total: threads.length,
464
- totalTraces: data.traces?.length || 0
539
+ totalTraces: data.traces?.length || 0,
465
540
  });
466
541
  } catch (error) {
467
542
  console.error('❌ Error reading traces:', error);
@@ -476,27 +551,27 @@ app.get('/api/traces', (req, res) => {
476
551
  app.get('/api/traces/executions/:executionId', (req, res) => {
477
552
  try {
478
553
  const tracesFile = path.join(process.cwd(), '.runflow', 'traces.json');
479
-
554
+
480
555
  if (!fs.existsSync(tracesFile)) {
481
556
  return res.status(404).json({ error: 'Execution not found' });
482
557
  }
483
-
558
+
484
559
  const data = JSON.parse(fs.readFileSync(tracesFile, 'utf-8'));
485
560
  const execution = data.executions?.[req.params.executionId];
486
-
561
+
487
562
  if (!execution) {
488
563
  return res.status(404).json({ error: 'Execution not found' });
489
564
  }
490
-
565
+
491
566
  // Build hierarchy of traces
492
567
  const traceMap = new Map();
493
568
  const rootTraces = [];
494
-
495
- execution.traces.forEach(trace => {
569
+
570
+ execution.traces.forEach((trace) => {
496
571
  traceMap.set(trace.traceId, { ...trace, children: [] });
497
572
  });
498
-
499
- execution.traces.forEach(trace => {
573
+
574
+ execution.traces.forEach((trace) => {
500
575
  const node = traceMap.get(trace.traceId);
501
576
  if (trace.parentTraceId) {
502
577
  const parent = traceMap.get(trace.parentTraceId);
@@ -509,7 +584,7 @@ app.get('/api/traces/executions/:executionId', (req, res) => {
509
584
  rootTraces.push(node);
510
585
  }
511
586
  });
512
-
587
+
513
588
  res.json({
514
589
  execution: {
515
590
  executionId: execution.executionId,
@@ -535,15 +610,15 @@ app.get('/api/traces/executions/:executionId', (req, res) => {
535
610
  app.delete('/api/traces', (req, res) => {
536
611
  try {
537
612
  const tracesFile = path.join(process.cwd(), '.runflow', 'traces.json');
538
-
613
+
539
614
  if (fs.existsSync(tracesFile)) {
540
615
  fs.unlinkSync(tracesFile);
541
616
  console.log('🗑️ Local traces cleared');
542
617
  }
543
-
544
- res.json({
618
+
619
+ res.json({
545
620
  success: true,
546
- message: 'Local traces cleared successfully'
621
+ message: 'Local traces cleared successfully',
547
622
  });
548
623
  } catch (error) {
549
624
  console.error('❌ Error clearing traces:', error);
@@ -552,7 +627,12 @@ app.delete('/api/traces', (req, res) => {
552
627
  });
553
628
 
554
629
  app.listen(CONFIG.port, () => {
555
- console.log('✅ Test server ready on port', CONFIG.port);
630
+ console.log('✅ [READY] Test server ready on port', CONFIG.port);
631
+ console.log('📂 [CWD]', process.cwd());
632
+ console.log('📄 [MAIN]', CONFIG.mainFile);
633
+ if (CONFIG.verbose) {
634
+ console.log('🔍 [VERBOSE] All logs will be shown');
635
+ }
556
636
  });
557
637
 
558
638
  process.on('SIGINT', () => {