@runflow-ai/cli 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/commands/agents/agents.command.js +244 -93
  2. package/dist/commands/agents/agents.command.js.map +1 -1
  3. package/dist/commands/login/login.command.js +23 -9
  4. package/dist/commands/login/login.command.js.map +1 -1
  5. package/dist/commands/prompts/prompts.command.js +13 -13
  6. package/dist/commands/prompts/prompts.command.js.map +1 -1
  7. package/dist/commands/test/test.command.d.ts +27 -0
  8. package/dist/commands/test/test.command.js +326 -0
  9. package/dist/commands/test/test.command.js.map +1 -0
  10. package/dist/commands/users/users.command.js +16 -19
  11. package/dist/commands/users/users.command.js.map +1 -1
  12. package/dist/common/api-client.js +4 -1
  13. package/dist/common/api-client.js.map +1 -1
  14. package/dist/common/banner.js +34 -1
  15. package/dist/common/banner.js.map +1 -1
  16. package/dist/common/config.d.ts +1 -1
  17. package/dist/common/config.js +42 -6
  18. package/dist/common/config.js.map +1 -1
  19. package/dist/common/logger.js +34 -1
  20. package/dist/common/logger.js.map +1 -1
  21. package/dist/common/system-check.d.ts +18 -0
  22. package/dist/common/system-check.js +118 -0
  23. package/dist/common/system-check.js.map +1 -0
  24. package/dist/common/ui.js +13 -4
  25. package/dist/common/ui.js.map +1 -1
  26. package/dist/common/validator.d.ts +13 -0
  27. package/dist/common/validator.js +191 -0
  28. package/dist/common/validator.js.map +1 -0
  29. package/dist/index.js +42 -5
  30. package/dist/index.js.map +1 -1
  31. package/package.json +10 -3
  32. package/scenarios/README.md +71 -0
  33. package/scenarios/memory-conversation.json +23 -0
  34. package/scenarios/test-messages-array.json +19 -0
  35. package/scenarios/test-metadata-custom.json +22 -0
  36. package/scenarios/webhook-hubspot-contact.json +40 -0
  37. package/scenarios/webhook-meta-messenger.json +33 -0
  38. package/scenarios/webhook-shopify.json +24 -0
  39. package/scenarios/webhook-slack-message.json +21 -0
  40. package/scenarios/webhook-stripe.json +22 -0
  41. package/scenarios/webhook-twilio-sms.json +21 -0
  42. package/scenarios/webhook-twilio-whatsapp.json +22 -0
  43. package/scenarios/webhook-zendesk-ticket.json +25 -0
  44. package/static/README.md +221 -0
  45. package/static/app.js +1381 -0
  46. package/static/frontend-server-template.js +24 -0
  47. package/static/index.html +340 -0
  48. package/static/runflow-logo.png +0 -0
  49. package/static/style.css +1354 -0
  50. package/static/test-server-template.js +561 -0
@@ -0,0 +1,561 @@
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
+ };
16
+
17
+ // ============================================================================
18
+ // Load rf.json (agent configuration)
19
+ // ============================================================================
20
+
21
+ function loadRfConfig() {
22
+ const rfConfigPath = path.join(process.cwd(), '.runflow', 'rf.json');
23
+
24
+ if (fs.existsSync(rfConfigPath)) {
25
+ try {
26
+ const config = JSON.parse(fs.readFileSync(rfConfigPath, 'utf8'));
27
+ console.log('✅ Loaded rf.json:', {
28
+ agentId: config.agentId,
29
+ tenantId: config.tenantId,
30
+ apiUrl: config.apiUrl
31
+ });
32
+ return config;
33
+ } catch (error) {
34
+ console.warn('⚠️ Could not parse rf.json:', error.message);
35
+ }
36
+ }
37
+
38
+ console.warn('⚠️ No rf.json found - using defaults');
39
+ return {
40
+ agentId: 'local-agent',
41
+ tenantId: 'test-local',
42
+ apiUrl: 'http://localhost:3001'
43
+ };
44
+ }
45
+
46
+ const RF_CONFIG = loadRfConfig();
47
+
48
+ // ============================================================================
49
+ // Context Generation (simula Execution Engine)
50
+ // ============================================================================
51
+
52
+ function generateExecutionId() {
53
+ return `exec_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
54
+ }
55
+
56
+ function generateRandomId() {
57
+ return Math.random().toString(36).substring(2, 8);
58
+ }
59
+
60
+ function generateThreadId(input) {
61
+ const companyId = input.companyId || 'test';
62
+
63
+ // 1. Explicit
64
+ if (input.entityType && input.entityValue) {
65
+ const cleanValue = String(input.entityValue).replace(/[^a-zA-Z0-9]/g, '_');
66
+ return `${input.entityType}_${companyId}_${cleanValue}`;
67
+ }
68
+
69
+ // 2. SessionId
70
+ if (input.sessionId) {
71
+ return `session_${companyId}_${input.sessionId}`;
72
+ }
73
+
74
+ // 3. Phone (WhatsApp/Twilio)
75
+ if (input.metadata?.phone || input.metadata?.From) {
76
+ const phone = (input.metadata.phone || input.metadata.From).replace(/\D/g, '');
77
+ return `phone_${companyId}_${phone}`;
78
+ }
79
+
80
+ // 4. Email
81
+ if (input.metadata?.email) {
82
+ return `email_${companyId}_${input.metadata.email}`;
83
+ }
84
+
85
+ // 5. ContactId (HubSpot)
86
+ if (input.metadata?.contactId) {
87
+ return `hubspot_contact_${companyId}_${input.metadata.contactId}`;
88
+ }
89
+
90
+ // 6. Fallback
91
+ return `thread_${companyId}_${Date.now()}_${generateRandomId()}`;
92
+ }
93
+
94
+ function inferEntityType(input) {
95
+ if (input.entityType) return input.entityType;
96
+ if (input.metadata?.phone || input.metadata?.From) return 'phone';
97
+ if (input.metadata?.email) return 'email';
98
+ if (input.metadata?.contactId) return 'hubspot_contact';
99
+ if (input.sessionId) return 'session';
100
+ return 'session';
101
+ }
102
+
103
+ function inferEntityValue(input) {
104
+ if (input.entityValue) return input.entityValue;
105
+ if (input.metadata?.phone) return input.metadata.phone;
106
+ if (input.metadata?.From) return input.metadata.From;
107
+ if (input.metadata?.email) return input.metadata.email;
108
+ if (input.metadata?.contactId) return input.metadata.contactId;
109
+ if (input.sessionId) return input.sessionId;
110
+ return null;
111
+ }
112
+
113
+ function inferUserId(input) {
114
+ if (input.userId) return input.userId;
115
+ if (input.metadata?.phone) return input.metadata.phone;
116
+ if (input.metadata?.From) return input.metadata.From;
117
+ if (input.metadata?.email) return input.metadata.email;
118
+ if (input.metadata?.contactId) return input.metadata.contactId;
119
+ return null;
120
+ }
121
+
122
+ console.log('🧪 Runflow Test Server');
123
+ console.log('📄 Testing file:', CONFIG.mainFile);
124
+ console.log('🔧 Server port:', CONFIG.port);
125
+ console.log('🎯 Agent ID:', RF_CONFIG.agentId);
126
+ console.log('🏢 Tenant ID:', RF_CONFIG.tenantId);
127
+
128
+ // Endpoint para testar o agente local
129
+ app.post('/api/chat', async (req, res) => {
130
+ try {
131
+ console.log('\n📥 [Test] Received:', req.body.message);
132
+ console.log('🔄 [Reload] Loading fresh code from', CONFIG.mainFile + '...');
133
+
134
+ // ============================================================================
135
+ // Generate Execution Context (simula Execution Engine)
136
+ // ============================================================================
137
+
138
+ const input = {
139
+ message: req.body.message,
140
+ sessionId: req.body.sessionId || `test_session_${Date.now()}`,
141
+ companyId: req.body.companyId || 'test',
142
+ userId: req.body.userId,
143
+ channel: req.body.channel || 'test',
144
+ metadata: req.body.metadata || {},
145
+ timestamp: new Date().toISOString()
146
+ };
147
+
148
+ const executionId = generateExecutionId();
149
+ const threadId = generateThreadId(input);
150
+ const entityType = inferEntityType(input);
151
+ const entityValue = inferEntityValue(input);
152
+ const userId = inferUserId(input);
153
+
154
+ console.log('🔍 [Context Generated]:', {
155
+ executionId,
156
+ threadId,
157
+ entityType,
158
+ entityValue: entityValue || '(none)',
159
+ userId: userId || '(none)',
160
+ });
161
+
162
+ // Executar TypeScript diretamente com tsx
163
+ const inputData = JSON.stringify(input);
164
+
165
+ const tsxProcess = spawn('npx', ['tsx', '--eval', `
166
+ import { main } from './${CONFIG.mainFile}';
167
+
168
+ const input = ${inputData};
169
+
170
+ main(input).then(result => {
171
+ console.log('🔵 RUNFLOW_RESULT_START');
172
+ console.log(JSON.stringify(result));
173
+ console.log('🔵 RUNFLOW_RESULT_END');
174
+ process.exit(0);
175
+ }).catch(error => {
176
+ console.log('🔴 RUNFLOW_ERROR_START');
177
+ console.log(JSON.stringify({ error: error.message }));
178
+ console.log('🔴 RUNFLOW_ERROR_END');
179
+ process.exit(1);
180
+ });
181
+ `], {
182
+ env: {
183
+ ...process.env,
184
+ // ✅ Passar context via env (igual Execution Engine)
185
+ RUNFLOW_EXECUTION_ID: executionId,
186
+ RUNFLOW_THREAD_ID: threadId,
187
+ RUNFLOW_API_URL: RF_CONFIG.apiUrl || 'http://localhost:3001',
188
+ RUNFLOW_TENANT_ID: RF_CONFIG.tenantId,
189
+ RUNFLOW_AGENT_ID: RF_CONFIG.agentId,
190
+ // ✅ NOVO: Flag para salvar traces localmente
191
+ RUNFLOW_LOCAL_TRACES: 'true',
192
+ RUNFLOW_ENV: 'development',
193
+ },
194
+ stdio: ['pipe', 'pipe', 'pipe']
195
+ });
196
+
197
+ let output = '';
198
+ let errorOutput = '';
199
+ let hasResponded = false;
200
+ let isCapturingResult = false;
201
+ let isCapturingError = false;
202
+ let resultLines = [];
203
+ let errorLines = [];
204
+
205
+ // Processar saída linha por linha
206
+ const processLine = (line) => {
207
+ if (line.includes('🔵 RUNFLOW_RESULT_START')) {
208
+ isCapturingResult = true;
209
+ resultLines = [];
210
+ return;
211
+ }
212
+
213
+ if (line.includes('🔵 RUNFLOW_RESULT_END')) {
214
+ isCapturingResult = false;
215
+
216
+ if (!hasResponded && resultLines.length > 0) {
217
+ try {
218
+ const jsonResult = JSON.parse(resultLines.join('\n'));
219
+ console.log('✅ [Result Captured]:', jsonResult);
220
+
221
+ res.json({
222
+ success: true,
223
+ data: jsonResult,
224
+ // ✅ Incluir context no response (para debugging)
225
+ observability: {
226
+ executionId,
227
+ threadId,
228
+ entityType,
229
+ entityValue,
230
+ userId,
231
+ },
232
+ timestamp: new Date().toISOString()
233
+ });
234
+ hasResponded = true;
235
+ console.log('✅ [Response] Sent to frontend successfully');
236
+ } catch (e) {
237
+ console.log('⚠️ [Result Parse Error]:', e.message);
238
+ }
239
+ }
240
+ return;
241
+ }
242
+
243
+ if (line.includes('🔴 RUNFLOW_ERROR_START')) {
244
+ isCapturingError = true;
245
+ errorLines = [];
246
+ return;
247
+ }
248
+
249
+ if (line.includes('🔴 RUNFLOW_ERROR_END')) {
250
+ isCapturingError = false;
251
+
252
+ if (!hasResponded && errorLines.length > 0) {
253
+ try {
254
+ const errorResult = JSON.parse(errorLines.join('\n'));
255
+ console.log('❌ [Error Captured]:', errorResult);
256
+
257
+ res.status(500).json({
258
+ success: false,
259
+ error: errorResult.error,
260
+ timestamp: new Date().toISOString()
261
+ });
262
+ hasResponded = true;
263
+ } catch (e) {
264
+ console.log('⚠️ [Error Parse Error]:', e.message);
265
+ }
266
+ }
267
+ return;
268
+ }
269
+
270
+ if (isCapturingResult) {
271
+ resultLines.push(line);
272
+ } else if (isCapturingError) {
273
+ errorLines.push(line);
274
+ }
275
+ };
276
+
277
+ tsxProcess.stdout.on('data', (data) => {
278
+ const chunk = data.toString();
279
+ console.log('📤 [stdout]:', chunk.trim());
280
+ output += chunk;
281
+
282
+ const lines = chunk.split('\n');
283
+ lines.forEach(line => {
284
+ if (line.trim()) {
285
+ processLine(line.trim());
286
+ }
287
+ });
288
+ });
289
+
290
+ tsxProcess.stderr.on('data', (data) => {
291
+ const chunk = data.toString();
292
+ console.log('📤 [stderr]:', chunk.trim());
293
+ errorOutput += chunk;
294
+ });
295
+
296
+ tsxProcess.on('close', (code) => {
297
+ console.log('🔚 [Process] Closed with code:', code);
298
+
299
+ if (!hasResponded) {
300
+ console.log('⚠️ [Fallback] No structured response captured');
301
+
302
+ if (code === 0) {
303
+ const lines = output.trim().split('\n');
304
+ let jsonResult = null;
305
+
306
+ for (let i = lines.length - 1; i >= 0; i--) {
307
+ const line = lines[i].trim();
308
+ if (line.startsWith('{') && line.endsWith('}')) {
309
+ try {
310
+ jsonResult = JSON.parse(line);
311
+ console.log('✅ [Fallback JSON]:', jsonResult);
312
+ break;
313
+ } catch (e) {
314
+ // Continue procurando
315
+ }
316
+ }
317
+ }
318
+
319
+ if (jsonResult) {
320
+ res.json({
321
+ success: true,
322
+ data: jsonResult,
323
+ timestamp: new Date().toISOString()
324
+ });
325
+ } else {
326
+ res.json({
327
+ success: true,
328
+ data: { message: 'Process completed but no result captured' },
329
+ timestamp: new Date().toISOString()
330
+ });
331
+ }
332
+ } else {
333
+ res.status(500).json({
334
+ success: false,
335
+ error: errorOutput || `Process failed with code ${code}`,
336
+ timestamp: new Date().toISOString()
337
+ });
338
+ }
339
+ }
340
+ });
341
+
342
+ } catch (error) {
343
+ console.error('❌ [Test] Error:', error.message);
344
+
345
+ res.status(500).json({
346
+ success: false,
347
+ error: error.message,
348
+ timestamp: new Date().toISOString()
349
+ });
350
+ }
351
+ });
352
+
353
+ // Health check
354
+ app.get('/api/health', (req, res) => {
355
+ res.json({
356
+ status: 'ok',
357
+ timestamp: new Date().toISOString(),
358
+ agent: CONFIG.mainFile,
359
+ type: 'test-server'
360
+ });
361
+ });
362
+
363
+ // Get test scenarios
364
+ app.get('/api/scenarios', (req, res) => {
365
+ const fs = require('fs');
366
+ const path = require('path');
367
+
368
+ const scenarios = [];
369
+
370
+ // 1. Load CLI default scenarios
371
+ try {
372
+ const cliScenariosPath = process.env.RUNFLOW_SCENARIOS_PATH;
373
+
374
+ if (cliScenariosPath && fs.existsSync(cliScenariosPath)) {
375
+ const files = fs.readdirSync(cliScenariosPath).filter(f => f.endsWith('.json'));
376
+
377
+ files.forEach(file => {
378
+ try {
379
+ const content = JSON.parse(fs.readFileSync(path.join(cliScenariosPath, file), 'utf8'));
380
+ scenarios.push({ ...content, source: 'cli', id: file.replace('.json', '') });
381
+ } catch (e) {
382
+ console.error('Error loading CLI scenario ' + file + ':', e.message);
383
+ }
384
+ });
385
+ }
386
+ } catch (error) {
387
+ console.error('Error loading CLI scenarios:', error.message);
388
+ }
389
+
390
+ // 2. Load project custom scenarios
391
+ try {
392
+ const projectScenariosPath = path.join(process.cwd(), 'test-scenarios');
393
+ if (fs.existsSync(projectScenariosPath)) {
394
+ const files = fs.readdirSync(projectScenariosPath).filter(f => f.endsWith('.json'));
395
+ files.forEach(file => {
396
+ try {
397
+ const content = JSON.parse(fs.readFileSync(path.join(projectScenariosPath, file), 'utf8'));
398
+ scenarios.push({ ...content, source: 'project', id: file.replace('.json', '') });
399
+ } catch (e) {
400
+ console.error('Error loading project scenario ' + file + ':', e.message);
401
+ }
402
+ });
403
+ }
404
+ } catch (error) {
405
+ console.error('Error loading project scenarios:', error.message);
406
+ }
407
+
408
+ res.json({ scenarios });
409
+ });
410
+
411
+ // ============================================================================
412
+ // OBSERVABILITY ENDPOINTS - Local Development
413
+ // ============================================================================
414
+
415
+ /**
416
+ * Get local traces (grouped by thread)
417
+ * Mimics: GET /api/v1/observability/threads
418
+ */
419
+ app.get('/api/traces', (req, res) => {
420
+ try {
421
+ const tracesFile = path.join(process.cwd(), '.runflow', 'traces.json');
422
+
423
+ if (!fs.existsSync(tracesFile)) {
424
+ return res.json({
425
+ threads: [],
426
+ total: 0,
427
+ message: 'No traces yet. Run your agent to generate traces.'
428
+ });
429
+ }
430
+
431
+ const data = JSON.parse(fs.readFileSync(tracesFile, 'utf-8'));
432
+
433
+ // Group executions by threadId
434
+ const threadMap = {};
435
+ Object.values(data.executions || {}).forEach(execution => {
436
+ const threadId = execution.threadId || 'unknown';
437
+
438
+ if (!threadMap[threadId]) {
439
+ threadMap[threadId] = {
440
+ threadId,
441
+ entityType: execution.entityType,
442
+ entityValue: execution.entityValue,
443
+ executions: [],
444
+ totalTraces: 0,
445
+ lastActivity: execution.startedAt,
446
+ };
447
+ }
448
+
449
+ threadMap[threadId].executions.push(execution);
450
+ threadMap[threadId].totalTraces += execution.traces?.length || 0;
451
+
452
+ // Update last activity
453
+ if (execution.startedAt > threadMap[threadId].lastActivity) {
454
+ threadMap[threadId].lastActivity = execution.startedAt;
455
+ }
456
+ });
457
+
458
+ const threads = Object.values(threadMap)
459
+ .sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
460
+
461
+ res.json({
462
+ threads,
463
+ total: threads.length,
464
+ totalTraces: data.traces?.length || 0
465
+ });
466
+ } catch (error) {
467
+ console.error('❌ Error reading traces:', error);
468
+ res.status(500).json({ error: error.message });
469
+ }
470
+ });
471
+
472
+ /**
473
+ * Get execution details with traces
474
+ * Mimics: GET /api/v1/observability/executions/:executionId
475
+ */
476
+ app.get('/api/traces/executions/:executionId', (req, res) => {
477
+ try {
478
+ const tracesFile = path.join(process.cwd(), '.runflow', 'traces.json');
479
+
480
+ if (!fs.existsSync(tracesFile)) {
481
+ return res.status(404).json({ error: 'Execution not found' });
482
+ }
483
+
484
+ const data = JSON.parse(fs.readFileSync(tracesFile, 'utf-8'));
485
+ const execution = data.executions?.[req.params.executionId];
486
+
487
+ if (!execution) {
488
+ return res.status(404).json({ error: 'Execution not found' });
489
+ }
490
+
491
+ // Build hierarchy of traces
492
+ const traceMap = new Map();
493
+ const rootTraces = [];
494
+
495
+ execution.traces.forEach(trace => {
496
+ traceMap.set(trace.traceId, { ...trace, children: [] });
497
+ });
498
+
499
+ execution.traces.forEach(trace => {
500
+ const node = traceMap.get(trace.traceId);
501
+ if (trace.parentTraceId) {
502
+ const parent = traceMap.get(trace.parentTraceId);
503
+ if (parent) {
504
+ parent.children.push(node);
505
+ } else {
506
+ rootTraces.push(node);
507
+ }
508
+ } else {
509
+ rootTraces.push(node);
510
+ }
511
+ });
512
+
513
+ res.json({
514
+ execution: {
515
+ executionId: execution.executionId,
516
+ threadId: execution.threadId,
517
+ entityType: execution.entityType,
518
+ entityValue: execution.entityValue,
519
+ userId: execution.userId,
520
+ agentName: execution.agentName,
521
+ startedAt: execution.startedAt,
522
+ totalTraces: execution.traces.length,
523
+ },
524
+ traces: rootTraces,
525
+ });
526
+ } catch (error) {
527
+ console.error('❌ Error reading execution:', error);
528
+ res.status(500).json({ error: error.message });
529
+ }
530
+ });
531
+
532
+ /**
533
+ * Clear local traces
534
+ */
535
+ app.delete('/api/traces', (req, res) => {
536
+ try {
537
+ const tracesFile = path.join(process.cwd(), '.runflow', 'traces.json');
538
+
539
+ if (fs.existsSync(tracesFile)) {
540
+ fs.unlinkSync(tracesFile);
541
+ console.log('🗑️ Local traces cleared');
542
+ }
543
+
544
+ res.json({
545
+ success: true,
546
+ message: 'Local traces cleared successfully'
547
+ });
548
+ } catch (error) {
549
+ console.error('❌ Error clearing traces:', error);
550
+ res.status(500).json({ error: error.message });
551
+ }
552
+ });
553
+
554
+ app.listen(CONFIG.port, () => {
555
+ console.log('✅ Test server ready on port', CONFIG.port);
556
+ });
557
+
558
+ process.on('SIGINT', () => {
559
+ console.log('\n🛑 Shutting down test server...');
560
+ process.exit(0);
561
+ });