@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.
- package/CLI-DOCS.md +89 -0
- package/QUICK-TEST-GUIDE.md +273 -0
- package/dist/commands/test/test.command.d.ts +19 -12
- package/dist/commands/test/test.command.js +703 -212
- package/dist/commands/test/test.command.js.map +1 -1
- package/package.json +3 -2
- package/static/dist-test/assets/favico.avif +0 -0
- package/static/dist-test/assets/logo_runflow_positive.png +0 -0
- package/static/dist-test/assets/main-ClrC9fUE.css +1 -0
- package/static/dist-test/assets/main-rM2NnEnW.js +53 -0
- package/static/dist-test/assets/runflow-logo.png +0 -0
- package/static/dist-test/index-test.html +16 -0
- package/static/dist-test/widget/runflow-widget.js +221 -0
- package/static/app.js +0 -1381
- package/static/frontend-server-template.js +0 -24
- package/static/index.html +0 -340
- package/static/style.css +0 -1354
- package/static/test-server-template.js +0 -641
|
@@ -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
|
-
});
|