@runflow-ai/cli 0.2.11 → 0.2.13

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.
@@ -1,641 +0,0 @@
1
- const express = require('express');
2
- const cors = require('cors');
3
- const path = require('path');
4
- const { spawn } = require('child_process');
5
- const fs = require('fs');
6
-
7
- const app = express();
8
- app.use(cors());
9
- app.use(express.json());
10
-
11
- // Configuração será injetada pela CLI
12
- const CONFIG = {
13
- port: process.env.RUNFLOW_PORT || 8547,
14
- mainFile: process.env.RUNFLOW_MAIN_FILE || 'main.ts',
15
- verbose: process.env.RUNFLOW_VERBOSE === 'true',
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
-
25
- // ============================================================================
26
- // Load rf.json (agent configuration)
27
- // ============================================================================
28
-
29
- function loadRfConfig() {
30
- const rfConfigPath = path.join(process.cwd(), '.runflow', 'rf.json');
31
-
32
- if (fs.existsSync(rfConfigPath)) {
33
- try {
34
- const config = JSON.parse(fs.readFileSync(rfConfigPath, 'utf8'));
35
- console.log('✅ Loaded rf.json:', {
36
- agentId: config.agentId,
37
- tenantId: config.tenantId,
38
- apiUrl: config.apiUrl,
39
- });
40
- return config;
41
- } catch (error) {
42
- console.warn('⚠️ Could not parse rf.json:', error.message);
43
- }
44
- }
45
-
46
- console.warn('⚠️ No rf.json found - using defaults');
47
- return {
48
- agentId: 'local-agent',
49
- tenantId: 'test-local',
50
- apiUrl: 'http://localhost:3001',
51
- };
52
- }
53
-
54
- const RF_CONFIG = loadRfConfig();
55
-
56
- // ============================================================================
57
- // Context Generation (simula Execution Engine)
58
- // ============================================================================
59
-
60
- function generateExecutionId() {
61
- return `exec_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
62
- }
63
-
64
- function generateRandomId() {
65
- return Math.random().toString(36).substring(2, 8);
66
- }
67
-
68
- function generateThreadId(input) {
69
- const companyId = input.companyId || 'test';
70
-
71
- // 1. Explicit
72
- if (input.entityType && input.entityValue) {
73
- const cleanValue = String(input.entityValue).replace(/[^a-zA-Z0-9]/g, '_');
74
- return `${input.entityType}_${companyId}_${cleanValue}`;
75
- }
76
-
77
- // 2. SessionId
78
- if (input.sessionId) {
79
- return `session_${companyId}_${input.sessionId}`;
80
- }
81
-
82
- // 3. Phone (WhatsApp/Twilio)
83
- if (input.metadata?.phone || input.metadata?.From) {
84
- const phone = (input.metadata.phone || input.metadata.From).replace(/\D/g, '');
85
- return `phone_${companyId}_${phone}`;
86
- }
87
-
88
- // 4. Email
89
- if (input.metadata?.email) {
90
- return `email_${companyId}_${input.metadata.email}`;
91
- }
92
-
93
- // 5. ContactId (HubSpot)
94
- if (input.metadata?.contactId) {
95
- return `hubspot_contact_${companyId}_${input.metadata.contactId}`;
96
- }
97
-
98
- // 6. Fallback
99
- return `thread_${companyId}_${Date.now()}_${generateRandomId()}`;
100
- }
101
-
102
- function inferEntityType(input) {
103
- if (input.entityType) return input.entityType;
104
- if (input.metadata?.phone || input.metadata?.From) return 'phone';
105
- if (input.metadata?.email) return 'email';
106
- if (input.metadata?.contactId) return 'hubspot_contact';
107
- if (input.sessionId) return 'session';
108
- return 'session';
109
- }
110
-
111
- function inferEntityValue(input) {
112
- if (input.entityValue) return input.entityValue;
113
- if (input.metadata?.phone) return input.metadata.phone;
114
- if (input.metadata?.From) return input.metadata.From;
115
- if (input.metadata?.email) return input.metadata.email;
116
- if (input.metadata?.contactId) return input.metadata.contactId;
117
- if (input.sessionId) return input.sessionId;
118
- return null;
119
- }
120
-
121
- function inferUserId(input) {
122
- if (input.userId) return input.userId;
123
- if (input.metadata?.phone) return input.metadata.phone;
124
- if (input.metadata?.From) return input.metadata.From;
125
- if (input.metadata?.email) return input.metadata.email;
126
- if (input.metadata?.contactId) return input.metadata.contactId;
127
- return null;
128
- }
129
-
130
- console.log('🧪 Runflow Test Server');
131
- console.log('📄 Testing file:', CONFIG.mainFile);
132
- console.log('🔧 Server port:', CONFIG.port);
133
- console.log('🎯 Agent ID:', RF_CONFIG.agentId);
134
- console.log('🏢 Tenant ID:', RF_CONFIG.tenantId);
135
-
136
- // Endpoint para testar o agente local
137
- app.post('/api/chat', async (req, res) => {
138
- try {
139
- console.log('\n📥 [Test] Received:', req.body.message);
140
- console.log('🔄 [Reload] Loading fresh code from', CONFIG.mainFile + '...');
141
-
142
- // ============================================================================
143
- // Generate Execution Context (simula Execution Engine)
144
- // ============================================================================
145
-
146
- const input = {
147
- message: req.body.message,
148
- sessionId: req.body.sessionId || `test_session_${Date.now()}`,
149
- companyId: req.body.companyId || 'test',
150
- userId: req.body.userId,
151
- channel: req.body.channel || 'test',
152
- metadata: req.body.metadata || {},
153
- timestamp: new Date().toISOString(),
154
- };
155
-
156
- const executionId = generateExecutionId();
157
- const threadId = generateThreadId(input);
158
- const entityType = inferEntityType(input);
159
- const entityValue = inferEntityValue(input);
160
- const userId = inferUserId(input);
161
-
162
- console.log('🔍 [Context Generated]:', {
163
- executionId,
164
- threadId,
165
- entityType,
166
- entityValue: entityValue || '(none)',
167
- userId: userId || '(none)',
168
- });
169
-
170
- // Executar TypeScript diretamente com tsx
171
- const inputData = JSON.stringify(input);
172
-
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}';
188
-
189
- const input = ${inputData};
190
-
191
- main(input).then(result => {
192
- console.log('🔵 RUNFLOW_RESULT_START');
193
- console.log(JSON.stringify(result));
194
- console.log('🔵 RUNFLOW_RESULT_END');
195
- process.exit(0);
196
- }).catch(error => {
197
- console.log('🔴 RUNFLOW_ERROR_START');
198
- console.log(JSON.stringify({ error: error.message, stack: error.stack }));
199
- console.log('🔴 RUNFLOW_ERROR_END');
200
- process.exit(1);
201
- });
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
212
- env: {
213
- ...process.env,
214
- // ✅ Passar context via env (igual Execution Engine)
215
- RUNFLOW_EXECUTION_ID: executionId,
216
- RUNFLOW_THREAD_ID: threadId,
217
- RUNFLOW_API_URL: RF_CONFIG.apiUrl || 'http://localhost:3001',
218
- RUNFLOW_TENANT_ID: RF_CONFIG.tenantId,
219
- RUNFLOW_AGENT_ID: RF_CONFIG.agentId,
220
- // ✅ NOVO: Flag para salvar traces localmente
221
- RUNFLOW_LOCAL_TRACES: 'true',
222
- RUNFLOW_ENV: 'development',
223
- },
224
- stdio: ['pipe', 'pipe', 'pipe'], // Sempre pipe para capturar resultado
225
- // No Windows, npx precisa de shell: true
226
- shell: process.platform === 'win32',
227
- });
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
-
247
- let output = '';
248
- let errorOutput = '';
249
- let hasResponded = false;
250
- let isCapturingResult = false;
251
- let isCapturingError = false;
252
- let resultLines = [];
253
- let errorLines = [];
254
-
255
- // Processar saída linha por linha
256
- const processLine = (line) => {
257
- if (line.includes('🔵 RUNFLOW_RESULT_START')) {
258
- isCapturingResult = true;
259
- resultLines = [];
260
- return;
261
- }
262
-
263
- if (line.includes('🔵 RUNFLOW_RESULT_END')) {
264
- isCapturingResult = false;
265
-
266
- if (!hasResponded && resultLines.length > 0) {
267
- try {
268
- const jsonResult = JSON.parse(resultLines.join('\n'));
269
- console.log('✅ [Result Captured]:', jsonResult);
270
-
271
- res.json({
272
- success: true,
273
- data: jsonResult,
274
- // ✅ Incluir context no response (para debugging)
275
- observability: {
276
- executionId,
277
- threadId,
278
- entityType,
279
- entityValue,
280
- userId,
281
- },
282
- timestamp: new Date().toISOString(),
283
- });
284
- hasResponded = true;
285
- console.log('✅ [Response] Sent to frontend successfully');
286
- } catch (e) {
287
- console.log('⚠️ [Result Parse Error]:', e.message);
288
- }
289
- }
290
- return;
291
- }
292
-
293
- if (line.includes('🔴 RUNFLOW_ERROR_START')) {
294
- isCapturingError = true;
295
- errorLines = [];
296
- return;
297
- }
298
-
299
- if (line.includes('🔴 RUNFLOW_ERROR_END')) {
300
- isCapturingError = false;
301
-
302
- if (!hasResponded && errorLines.length > 0) {
303
- try {
304
- const errorResult = JSON.parse(errorLines.join('\n'));
305
- console.log('❌ [Error Captured]:', errorResult);
306
-
307
- res.status(500).json({
308
- success: false,
309
- error: errorResult.error,
310
- timestamp: new Date().toISOString(),
311
- });
312
- hasResponded = true;
313
- } catch (e) {
314
- console.log('⚠️ [Error Parse Error]:', e.message);
315
- }
316
- }
317
- return;
318
- }
319
-
320
- if (isCapturingResult) {
321
- resultLines.push(line);
322
- } else if (isCapturingError) {
323
- errorLines.push(line);
324
- }
325
- };
326
-
327
- // Sempre capturar output, mas só mostrar logs detalhados se verbose
328
- tsxProcess.stdout?.on('data', (data) => {
329
- const chunk = data.toString();
330
- if (CONFIG.verbose) {
331
- console.log('📤 [TSX stdout]:', chunk.trim());
332
- }
333
- output += chunk;
334
-
335
- const lines = chunk.split('\n');
336
- lines.forEach((line) => {
337
- if (line.trim()) {
338
- processLine(line.trim());
339
- }
340
- });
341
- });
342
-
343
- tsxProcess.stderr?.on('data', (data) => {
344
- const chunk = data.toString();
345
- if (CONFIG.verbose) {
346
- console.log('📤 [TSX stderr]:', chunk.trim());
347
- }
348
- errorOutput += chunk;
349
- });
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
-
363
- tsxProcess.on('close', (code) => {
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
-
372
- if (!hasResponded) {
373
- console.log('⚠️ [Fallback] No structured response captured');
374
-
375
- if (code === 0) {
376
- const lines = output.trim().split('\n');
377
- let jsonResult = null;
378
-
379
- for (let i = lines.length - 1; i >= 0; i--) {
380
- const line = lines[i].trim();
381
- if (line.startsWith('{') && line.endsWith('}')) {
382
- try {
383
- jsonResult = JSON.parse(line);
384
- console.log('✅ [Fallback JSON]:', jsonResult);
385
- break;
386
- } catch (e) {
387
- // Continue procurando
388
- }
389
- }
390
- }
391
-
392
- if (jsonResult) {
393
- res.json({
394
- success: true,
395
- data: jsonResult,
396
- timestamp: new Date().toISOString(),
397
- });
398
- } else {
399
- res.json({
400
- success: true,
401
- data: { message: 'Process completed but no result captured' },
402
- timestamp: new Date().toISOString(),
403
- });
404
- }
405
- } else {
406
- res.status(500).json({
407
- success: false,
408
- error: errorOutput || `Process failed with code ${code}`,
409
- timestamp: new Date().toISOString(),
410
- });
411
- }
412
- }
413
- });
414
- } catch (error) {
415
- console.error('❌ [Test] Error:', error.message);
416
-
417
- res.status(500).json({
418
- success: false,
419
- error: error.message,
420
- timestamp: new Date().toISOString(),
421
- });
422
- }
423
- });
424
-
425
- // Health check
426
- app.get('/api/health', (req, res) => {
427
- res.json({
428
- status: 'ok',
429
- timestamp: new Date().toISOString(),
430
- agent: CONFIG.mainFile,
431
- type: 'test-servers',
432
- });
433
- });
434
-
435
- // Get test scenarios
436
- app.get('/api/scenarios', (req, res) => {
437
- const fs = require('fs');
438
- const path = require('path');
439
-
440
- const scenarios = [];
441
-
442
- // 1. Load CLI default scenarios
443
- try {
444
- const cliScenariosPath = process.env.RUNFLOW_SCENARIOS_PATH;
445
-
446
- if (cliScenariosPath && fs.existsSync(cliScenariosPath)) {
447
- const files = fs.readdirSync(cliScenariosPath).filter((f) => f.endsWith('.json'));
448
-
449
- files.forEach((file) => {
450
- try {
451
- const content = JSON.parse(fs.readFileSync(path.join(cliScenariosPath, file), 'utf8'));
452
- scenarios.push({ ...content, source: 'cli', id: file.replace('.json', '') });
453
- } catch (e) {
454
- console.error('Error loading CLI scenario ' + file + ':', e.message);
455
- }
456
- });
457
- }
458
- } catch (error) {
459
- console.error('Error loading CLI scenarios:', error.message);
460
- }
461
-
462
- // 2. Load project custom scenarios
463
- try {
464
- const projectScenariosPath = path.join(process.cwd(), 'test-scenarios');
465
- if (fs.existsSync(projectScenariosPath)) {
466
- const files = fs.readdirSync(projectScenariosPath).filter((f) => f.endsWith('.json'));
467
- files.forEach((file) => {
468
- try {
469
- const content = JSON.parse(
470
- fs.readFileSync(path.join(projectScenariosPath, file), 'utf8'),
471
- );
472
- scenarios.push({ ...content, source: 'project', id: file.replace('.json', '') });
473
- } catch (e) {
474
- console.error('Error loading project scenario ' + file + ':', e.message);
475
- }
476
- });
477
- }
478
- } catch (error) {
479
- console.error('Error loading project scenarios:', error.message);
480
- }
481
-
482
- res.json({ scenarios });
483
- });
484
-
485
- // ============================================================================
486
- // OBSERVABILITY ENDPOINTS - Local Development
487
- // ============================================================================
488
-
489
- /**
490
- * Get local traces (grouped by thread)
491
- * Mimics: GET /api/v1/observability/threads
492
- */
493
- app.get('/api/traces', (req, res) => {
494
- try {
495
- const tracesFile = path.join(process.cwd(), '.runflow', 'traces.json');
496
-
497
- if (!fs.existsSync(tracesFile)) {
498
- return res.json({
499
- threads: [],
500
- total: 0,
501
- message: 'No traces yet. Run your agent to generate traces.',
502
- });
503
- }
504
-
505
- const data = JSON.parse(fs.readFileSync(tracesFile, 'utf-8'));
506
-
507
- // Group executions by threadId
508
- const threadMap = {};
509
- Object.values(data.executions || {}).forEach((execution) => {
510
- const threadId = execution.threadId || 'unknown';
511
-
512
- if (!threadMap[threadId]) {
513
- threadMap[threadId] = {
514
- threadId,
515
- entityType: execution.entityType,
516
- entityValue: execution.entityValue,
517
- executions: [],
518
- totalTraces: 0,
519
- lastActivity: execution.startedAt,
520
- };
521
- }
522
-
523
- threadMap[threadId].executions.push(execution);
524
- threadMap[threadId].totalTraces += execution.traces?.length || 0;
525
-
526
- // Update last activity
527
- if (execution.startedAt > threadMap[threadId].lastActivity) {
528
- threadMap[threadId].lastActivity = execution.startedAt;
529
- }
530
- });
531
-
532
- const threads = Object.values(threadMap).sort(
533
- (a, b) => new Date(b.lastActivity) - new Date(a.lastActivity),
534
- );
535
-
536
- res.json({
537
- threads,
538
- total: threads.length,
539
- totalTraces: data.traces?.length || 0,
540
- });
541
- } catch (error) {
542
- console.error('❌ Error reading traces:', error);
543
- res.status(500).json({ error: error.message });
544
- }
545
- });
546
-
547
- /**
548
- * Get execution details with traces
549
- * Mimics: GET /api/v1/observability/executions/:executionId
550
- */
551
- app.get('/api/traces/executions/:executionId', (req, res) => {
552
- try {
553
- const tracesFile = path.join(process.cwd(), '.runflow', 'traces.json');
554
-
555
- if (!fs.existsSync(tracesFile)) {
556
- return res.status(404).json({ error: 'Execution not found' });
557
- }
558
-
559
- const data = JSON.parse(fs.readFileSync(tracesFile, 'utf-8'));
560
- const execution = data.executions?.[req.params.executionId];
561
-
562
- if (!execution) {
563
- return res.status(404).json({ error: 'Execution not found' });
564
- }
565
-
566
- // Build hierarchy of traces
567
- const traceMap = new Map();
568
- const rootTraces = [];
569
-
570
- execution.traces.forEach((trace) => {
571
- traceMap.set(trace.traceId, { ...trace, children: [] });
572
- });
573
-
574
- execution.traces.forEach((trace) => {
575
- const node = traceMap.get(trace.traceId);
576
- if (trace.parentTraceId) {
577
- const parent = traceMap.get(trace.parentTraceId);
578
- if (parent) {
579
- parent.children.push(node);
580
- } else {
581
- rootTraces.push(node);
582
- }
583
- } else {
584
- rootTraces.push(node);
585
- }
586
- });
587
-
588
- res.json({
589
- execution: {
590
- executionId: execution.executionId,
591
- threadId: execution.threadId,
592
- entityType: execution.entityType,
593
- entityValue: execution.entityValue,
594
- userId: execution.userId,
595
- agentName: execution.agentName,
596
- startedAt: execution.startedAt,
597
- totalTraces: execution.traces.length,
598
- },
599
- traces: rootTraces,
600
- });
601
- } catch (error) {
602
- console.error('❌ Error reading execution:', error);
603
- res.status(500).json({ error: error.message });
604
- }
605
- });
606
-
607
- /**
608
- * Clear local traces
609
- */
610
- app.delete('/api/traces', (req, res) => {
611
- try {
612
- const tracesFile = path.join(process.cwd(), '.runflow', 'traces.json');
613
-
614
- if (fs.existsSync(tracesFile)) {
615
- fs.unlinkSync(tracesFile);
616
- console.log('🗑️ Local traces cleared');
617
- }
618
-
619
- res.json({
620
- success: true,
621
- message: 'Local traces cleared successfully',
622
- });
623
- } catch (error) {
624
- console.error('❌ Error clearing traces:', error);
625
- res.status(500).json({ error: error.message });
626
- }
627
- });
628
-
629
- app.listen(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
- }
636
- });
637
-
638
- process.on('SIGINT', () => {
639
- console.log('\n🛑 Shutting down test server...');
640
- process.exit(0);
641
- });