@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.
@@ -50,70 +50,74 @@ const nest_commander_1 = require("nest-commander");
50
50
  const common_1 = require("@nestjs/common");
51
51
  const fs = __importStar(require("fs"));
52
52
  const path = __importStar(require("path"));
53
- const child_process_1 = require("child_process");
54
53
  const chalk_1 = __importDefault(require("chalk"));
55
- const ora_1 = __importDefault(require("ora"));
56
- const validator_1 = require("../../common/validator");
54
+ const express_1 = __importDefault(require("express"));
55
+ const cors_1 = __importDefault(require("cors"));
56
+ const crypto_1 = require("crypto");
57
57
  let TestCommand = class TestCommand extends nest_commander_1.CommandRunner {
58
58
  constructor() {
59
59
  super(...arguments);
60
60
  this.server = null;
61
- this.frontend = null;
61
+ this.app = null;
62
+ this.storageDir = '';
62
63
  }
63
64
  async run(passedParam, options) {
64
65
  try {
65
- const port = options?.port || 8547;
66
- const host = options?.host || 'localhost';
67
- const shouldOpen = options?.open !== false;
68
- const verbose = true;
69
- console.log('๐Ÿ” [DEBUG] Options received:', options);
70
- console.log('๐Ÿ” [DEBUG] Verbose flag:', verbose);
71
- console.log(chalk_1.default.cyan.bold('๐Ÿงช Runflow Test Server'));
72
- console.log(chalk_1.default.gray('Starting local development environment...\n'));
73
- const validation = (0, validator_1.validateForTest)();
74
- if (!validation.isValid) {
75
- console.error(chalk_1.default.red('\nโŒ Project validation failed!'));
76
- validation.errors.forEach((error) => {
77
- console.error(chalk_1.default.red(` โ€ข ${error}`));
78
- });
79
- console.error(chalk_1.default.gray('\n๐Ÿ“š Documentation: https://docs.runflow.ai\n'));
66
+ console.log(chalk_1.default.cyan.bold('\n๐Ÿงช Runflow Test Server'));
67
+ console.log(chalk_1.default.gray('Starting local observability server...\n'));
68
+ const config = this.loadRunflowConfig();
69
+ if (!config || !config.agentId) {
70
+ console.error(chalk_1.default.red('โŒ No agentId found in .runflow/rf.json'));
71
+ console.error(chalk_1.default.gray(' Run "rf clone <agentId>" first\n'));
80
72
  process.exit(1);
81
73
  }
82
- const mainFile = validation.mainFile;
83
- const spinner = verbose ? null : (0, ora_1.default)('Initializing...').start();
84
- try {
85
- await this.ensureDependencies(spinner);
86
- await this.startTestServer(port, mainFile, verbose, spinner);
87
- await this.startFrontend(port + 1000, port, verbose, spinner);
88
- if (spinner) {
89
- spinner.succeed('Ready!');
90
- }
91
- if (!verbose) {
92
- console.clear();
74
+ console.log(chalk_1.default.green('โœ“ Found agent config'));
75
+ console.log(chalk_1.default.gray(` Agent: ${config.agentName || config.agentId}\n`));
76
+ this.storageDir = path.join(process.cwd(), '.runflow', 'traces');
77
+ this.ensureStorageDir();
78
+ const basePort = options?.port || this.getRandomPort();
79
+ let port = null;
80
+ let attempts = 0;
81
+ const maxAttempts = 3;
82
+ while (attempts < maxAttempts && !port) {
83
+ const tryPort = basePort + attempts;
84
+ try {
85
+ await this.startObservabilityServer(tryPort);
86
+ port = tryPort;
87
+ console.log(chalk_1.default.green(`โœ“ Local server running on port ${port}\n`));
93
88
  }
94
- console.log(chalk_1.default.green.bold('โœ… Test Interface Running!'));
95
- console.log('');
96
- console.log(chalk_1.default.cyan(`๐ŸŒ Open: http://${host}:${port + 1000}`));
97
- console.log(chalk_1.default.gray(`๐Ÿ“„ Testing: ${mainFile}`));
98
- console.log('');
99
- console.log(chalk_1.default.gray('Press Ctrl+C to stop'));
100
- if (shouldOpen) {
101
- this.openBrowser(`http://${host}:${port + 1000}`);
89
+ catch (error) {
90
+ attempts++;
91
+ if (attempts < maxAttempts) {
92
+ console.log(chalk_1.default.yellow(`โš ๏ธ Port ${tryPort} in use, trying ${tryPort + 1}...`));
93
+ }
102
94
  }
103
- this.setupCleanupHandlers();
104
- await this.keepAlive();
105
95
  }
106
- catch (error) {
107
- if (spinner) {
108
- spinner.fail('Failed to start test environment');
109
- }
110
- console.error(chalk_1.default.red(`โŒ Error: ${error instanceof Error ? error.message : 'Unknown error'}`));
111
- await this.cleanup();
96
+ if (!port) {
97
+ console.error(chalk_1.default.red(`โŒ Failed to start server after ${maxAttempts} attempts`));
98
+ console.error(chalk_1.default.gray(' All ports are busy. Try again later.\n'));
112
99
  process.exit(1);
113
100
  }
101
+ let portalUrl = `http://localhost:${port}/agents/${config.agentId}/test-monitor?apiUrl=http://localhost:${port}`;
102
+ if (config.agentName) {
103
+ portalUrl += `&agentName=${encodeURIComponent(config.agentName)}`;
104
+ }
105
+ if (options?.noBrowser !== true) {
106
+ console.log(chalk_1.default.green('โœ“ Opening browser...\n'));
107
+ this.openBrowser(portalUrl);
108
+ }
109
+ console.log(chalk_1.default.cyan.bold('๐Ÿ“Š Test Portal Ready!'));
110
+ console.log('');
111
+ console.log(chalk_1.default.cyan(` ๐ŸŒ Portal: ${portalUrl}`));
112
+ console.log(chalk_1.default.gray(` ๐Ÿ“ก API: http://localhost:${port}`));
113
+ console.log(chalk_1.default.gray(` ๐Ÿš€ Execute: POST http://localhost:${port}/api/v1/agents/execute`));
114
+ console.log(chalk_1.default.gray(` ๐Ÿ’พ Storage: ${this.storageDir}`));
115
+ console.log('');
116
+ console.log(chalk_1.default.gray('Press Ctrl+C to stop\n'));
117
+ this.setupCleanupHandlers();
118
+ await this.keepAlive();
114
119
  }
115
120
  catch (error) {
116
- console.log('deu erro aqui');
117
121
  console.error(chalk_1.default.red(`โŒ Error: ${error instanceof Error ? error.message : 'Unknown error'}`));
118
122
  await this.cleanup();
119
123
  process.exit(1);
@@ -126,14 +130,7 @@ let TestCommand = class TestCommand extends nest_commander_1.CommandRunner {
126
130
  }
127
131
  return port;
128
132
  }
129
- parseHost(val) {
130
- return val;
131
- }
132
- parseOpen() {
133
- return false;
134
- }
135
- parseVerbose(val) {
136
- console.log('๐Ÿ” [PARSER] parseVerbose called with:', val);
133
+ parseNoBrowser() {
137
134
  return true;
138
135
  }
139
136
  loadRunflowConfig() {
@@ -148,197 +145,709 @@ let TestCommand = class TestCommand extends nest_commander_1.CommandRunner {
148
145
  }
149
146
  return null;
150
147
  }
151
- async ensureDependencies(spinner) {
152
- if (spinner) {
153
- spinner.text = 'Checking dependencies...';
154
- }
155
- else {
156
- console.log('๐Ÿ” Checking dependencies...');
157
- }
158
- const packageJsonPath = path.join(process.cwd(), 'package.json');
159
- if (!fs.existsSync(packageJsonPath)) {
160
- throw new Error("No package.json found. Make sure you're in a valid Node.js project.");
161
- }
162
- const nodeModulesPath = path.join(process.cwd(), 'node_modules');
163
- if (!fs.existsSync(nodeModulesPath)) {
164
- if (spinner) {
165
- spinner.text = 'Installing dependencies...';
148
+ async executeAgent(message, threadId, sessionId, userId) {
149
+ try {
150
+ const possibleFiles = [
151
+ path.join(process.cwd(), 'agent.ts'),
152
+ path.join(process.cwd(), 'agent.js'),
153
+ path.join(process.cwd(), 'src', 'agent.ts'),
154
+ path.join(process.cwd(), 'src', 'agent.js'),
155
+ path.join(process.cwd(), 'main.ts'),
156
+ path.join(process.cwd(), 'main.js'),
157
+ ];
158
+ let agentFile = null;
159
+ for (const file of possibleFiles) {
160
+ if (fs.existsSync(file)) {
161
+ agentFile = file;
162
+ break;
163
+ }
164
+ }
165
+ if (!agentFile) {
166
+ throw new Error('Agent file not found. Expected agent.ts, agent.js, main.ts, or main.js in project root or src/');
167
+ }
168
+ process.env.RUNFLOW_LOCAL_TRACES = 'true';
169
+ process.env.RUNFLOW_ENV = 'development';
170
+ process.env.RUNFLOW_VERBOSE_TRACING = 'true';
171
+ let agentModule;
172
+ if (agentFile.endsWith('.ts')) {
173
+ require('tsx/cjs');
174
+ agentModule = require(agentFile);
166
175
  }
167
176
  else {
168
- console.log('๐Ÿ“ฆ Installing dependencies...');
177
+ agentModule = require(agentFile);
178
+ }
179
+ const config = this.loadRunflowConfig();
180
+ const input = {
181
+ message,
182
+ sessionId,
183
+ userId,
184
+ channel: 'local-test',
185
+ threadId,
186
+ metadata: {
187
+ timestamp: new Date().toISOString(),
188
+ source: 'local-cli',
189
+ agentId: config?.agentId,
190
+ }
191
+ };
192
+ let result;
193
+ if (typeof agentModule.main === 'function') {
194
+ result = await agentModule.main(input);
169
195
  }
170
- await this.runCommand('npm', ['install'], { stdio: 'pipe' });
196
+ else if (typeof agentModule.default === 'function') {
197
+ result = await agentModule.default(input);
198
+ }
199
+ else {
200
+ const agent = agentModule.agent || agentModule.default || agentModule;
201
+ if (typeof agent === 'function') {
202
+ const agentInstance = agent();
203
+ if (agentInstance && typeof agentInstance.process === 'function') {
204
+ result = await agentInstance.process(input);
205
+ }
206
+ else {
207
+ throw new Error('No valid export found. Expected: export async function main(input), export default function, or agent with process() method');
208
+ }
209
+ }
210
+ else if (agent && typeof agent.process === 'function') {
211
+ result = await agent.process(input);
212
+ }
213
+ else {
214
+ throw new Error('No valid export found. Expected: export async function main(input), export default function, or agent with process() method');
215
+ }
216
+ }
217
+ await new Promise((resolve) => setTimeout(resolve, 3000));
218
+ const tracesFile = path.join(process.cwd(), '.runflow', 'traces.json');
219
+ if (fs.existsSync(tracesFile)) {
220
+ try {
221
+ const content = fs.readFileSync(tracesFile, 'utf-8');
222
+ const data = JSON.parse(content);
223
+ const execId = result.executionId || result.metadata?.executionId || `exec-${Date.now()}`;
224
+ const execution = data.executions?.[execId];
225
+ if (execution) {
226
+ console.log(chalk_1.default.gray(` Traces saved: ${execution.traces?.length || 0}`));
227
+ const traceTypes = execution.traces?.map((t) => t.type) || [];
228
+ console.log(chalk_1.default.gray(` Types: ${[...new Set(traceTypes)].join(', ')}`));
229
+ }
230
+ }
231
+ catch (error) {
232
+ }
233
+ }
234
+ return {
235
+ executionId: result.executionId || result.metadata?.executionId || `exec-${Date.now()}`,
236
+ message: result.message || result.output || result,
237
+ metadata: result.metadata || {},
238
+ };
239
+ }
240
+ catch (error) {
241
+ throw new Error(`Failed to execute agent: ${error.message}`);
171
242
  }
172
243
  }
173
- async startTestServer(port, mainFile, verbose, spinner) {
174
- if (spinner) {
175
- spinner.text = 'Starting test server...';
244
+ getRandomPort() {
245
+ return Math.floor(Math.random() * 1000) + 3000;
246
+ }
247
+ generateSessionId(req, bodySessionId) {
248
+ if (bodySessionId) {
249
+ return bodySessionId;
250
+ }
251
+ const headerSessionId = req.headers['x-session-id'];
252
+ if (headerSessionId) {
253
+ return headerSessionId;
176
254
  }
177
- else {
178
- console.log('๐Ÿš€ Starting test server...');
255
+ const cookieHeader = req.headers['cookie'];
256
+ if (cookieHeader) {
257
+ const cookies = cookieHeader.split(';').reduce((acc, cookie) => {
258
+ const [key, value] = cookie.trim().split('=');
259
+ acc[key] = value;
260
+ return acc;
261
+ }, {});
262
+ if (cookies['runflow_session']) {
263
+ return cookies['runflow_session'];
264
+ }
179
265
  }
180
- const serverCliDir = path.join(__dirname, '..', '..', '..');
181
- const serverTemplatePath = path.join(serverCliDir, 'static', 'test-server-template.js');
182
- if (!fs.existsSync(serverTemplatePath)) {
183
- throw new Error('Test server template not found. Please ensure the CLI is properly installed.');
266
+ const fingerprint = this.createFingerprint(req);
267
+ return `auto_${fingerprint}_${Date.now()}`;
268
+ }
269
+ generateUserId(req, sessionId) {
270
+ const cookieHeader = req.headers['cookie'];
271
+ if (cookieHeader) {
272
+ const cookies = cookieHeader.split(';').reduce((acc, cookie) => {
273
+ const [key, value] = cookie.trim().split('=');
274
+ acc[key] = value;
275
+ return acc;
276
+ }, {});
277
+ if (cookies['runflow_user']) {
278
+ return cookies['runflow_user'];
279
+ }
184
280
  }
185
- const scenariosPath = path.join(serverCliDir, 'scenarios');
186
- this.server = (0, child_process_1.spawn)('node', [serverTemplatePath], {
187
- stdio: 'pipe',
188
- env: {
189
- ...process.env,
190
- NODE_ENV: 'development',
191
- RUNFLOW_PORT: port.toString(),
192
- RUNFLOW_MAIN_FILE: mainFile,
193
- RUNFLOW_SCENARIOS_PATH: scenariosPath,
194
- RUNFLOW_VERBOSE: verbose ? 'true' : 'false',
195
- },
196
- });
197
- if (verbose) {
198
- console.log(chalk_1.default.gray('[DEBUG] Verbose mode enabled - attaching log listeners'));
281
+ return sessionId;
282
+ }
283
+ createFingerprint(req) {
284
+ const components = [
285
+ req.headers['user-agent'] || '',
286
+ req.headers['accept-language'] || '',
287
+ req.ip || req.socket.remoteAddress || '',
288
+ ].join('|');
289
+ return (0, crypto_1.createHash)('md5').update(components).digest('hex').substring(0, 12);
290
+ }
291
+ ensureStorageDir() {
292
+ if (!fs.existsSync(this.storageDir)) {
293
+ fs.mkdirSync(this.storageDir, { recursive: true });
199
294
  }
200
- this.server.stdout?.on('data', (data) => {
201
- const output = data.toString();
202
- if (verbose) {
203
- process.stdout.write(`[SERVER] ${output}`);
295
+ }
296
+ async startObservabilityServer(port) {
297
+ return new Promise((resolve, reject) => {
298
+ this.app = (0, express_1.default)();
299
+ this.app.use((0, cors_1.default)({
300
+ origin: true,
301
+ credentials: true,
302
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
303
+ allowedHeaders: ['Content-Type', 'X-Thread-Id', 'X-Session-Id', 'Authorization', 'Cookie'],
304
+ exposedHeaders: ['Content-Length', 'X-Request-Id', 'X-Session-ID', 'X-User-ID', 'X-Timestamp'],
305
+ maxAge: 86400,
306
+ }));
307
+ this.app.use(express_1.default.json());
308
+ const distPath = path.join(__dirname, '..', '..', '..', 'static', 'dist-test');
309
+ this.app.use('/assets', express_1.default.static(path.join(distPath, 'assets')));
310
+ this.app.use('/widget', express_1.default.static(path.join(distPath, 'widget')));
311
+ this.app.get('/health', (req, res) => {
312
+ res.status(200).json({ status: 'ok', mode: 'local', version: '1.0.0' });
313
+ });
314
+ this.setupObservabilityEndpoints();
315
+ this.app.get('*', (req, res) => {
316
+ const indexPath = path.join(distPath, 'index-test.html');
317
+ res.sendFile(indexPath);
318
+ });
319
+ this.server = this.app.listen(port, () => {
320
+ resolve();
321
+ });
322
+ this.server.on('error', (error) => {
323
+ if (error.code === 'EADDRINUSE') {
324
+ reject(new Error(`Port ${port} is already in use`));
325
+ }
326
+ else {
327
+ reject(error);
328
+ }
329
+ });
330
+ });
331
+ }
332
+ setupObservabilityEndpoints() {
333
+ if (!this.app)
334
+ return;
335
+ this.app.post('/api/v1/agents/execute', async (req, res) => {
336
+ try {
337
+ const { message, threadId, sessionId: bodySessionId } = req.body;
338
+ if (!message) {
339
+ return res.status(400).json({
340
+ success: false,
341
+ error: 'message is required'
342
+ });
343
+ }
344
+ const normalizedMessage = message.toLowerCase().trim();
345
+ if (normalizedMessage === 'ping' || normalizedMessage === 'pong') {
346
+ return res.json({
347
+ success: true,
348
+ executionId: `ping-${Date.now()}`,
349
+ message: 'pong',
350
+ sessionId: 'ping-session',
351
+ userId: 'ping-user',
352
+ metadata: { isPing: true },
353
+ });
354
+ }
355
+ const sessionId = this.generateSessionId(req, bodySessionId);
356
+ const userId = this.generateUserId(req, sessionId);
357
+ console.log(chalk_1.default.gray(`๐Ÿ“จ [${new Date().toISOString()}] Executing agent...`));
358
+ console.log(chalk_1.default.gray(` Message: ${message.substring(0, 50)}${message.length > 50 ? '...' : ''}`));
359
+ if (threadId) {
360
+ console.log(chalk_1.default.gray(` Thread: ${threadId}`));
361
+ }
362
+ console.log(chalk_1.default.gray(` Session: ${sessionId}`));
363
+ const result = await this.executeAgent(message, threadId || sessionId, sessionId, userId);
364
+ console.log(chalk_1.default.green(`โœ“ [${new Date().toISOString()}] Agent executed successfully`));
365
+ console.log(chalk_1.default.gray(` Execution ID: ${result.executionId}`));
366
+ console.log('');
367
+ res.setHeader('X-Session-ID', sessionId);
368
+ res.setHeader('X-User-ID', userId);
369
+ res.setHeader('X-Timestamp', new Date().toISOString());
370
+ res.setHeader('Set-Cookie', [
371
+ `runflow_session=${sessionId}; Path=/; HttpOnly; SameSite=Lax`,
372
+ `runflow_user=${userId}; Path=/; HttpOnly; SameSite=Lax`,
373
+ ]);
374
+ res.json({
375
+ success: true,
376
+ executionId: result.executionId,
377
+ message: result.message,
378
+ sessionId: sessionId,
379
+ userId: userId,
380
+ metadata: result.metadata,
381
+ });
382
+ }
383
+ catch (error) {
384
+ console.error(chalk_1.default.red(`โŒ [${new Date().toISOString()}] Agent execution failed:`));
385
+ console.error(chalk_1.default.red(` ${error.message}`));
386
+ console.log('');
387
+ res.status(500).json({
388
+ success: false,
389
+ error: error.message
390
+ });
204
391
  }
205
392
  });
206
- this.server.stderr?.on('data', (data) => {
207
- const error = data.toString();
208
- if (verbose) {
209
- process.stderr.write(`[SERVER-ERR] ${error}`);
393
+ this.app.get('/api/v1/observability/threads', async (req, res) => {
394
+ try {
395
+ const { agentId, threadId, limit = 50 } = req.query;
396
+ if (!agentId) {
397
+ return res.status(400).json({ error: 'agentId is required' });
398
+ }
399
+ const threads = await this.getThreads(agentId, threadId, parseInt(limit));
400
+ res.json({ threads, total: threads.length });
210
401
  }
211
- else {
212
- const trimmed = error.trim();
213
- if (trimmed.includes('Error:') || trimmed.includes('EADDRINUSE')) {
214
- console.error(chalk_1.default.red(`Server Error: ${trimmed}`));
402
+ catch (error) {
403
+ console.error(chalk_1.default.red(`โŒ Error fetching threads: ${error.message}`));
404
+ res.status(500).json({ error: error.message });
405
+ }
406
+ });
407
+ this.app.get('/api/v1/observability/threads/:threadId/executions', async (req, res) => {
408
+ try {
409
+ const { threadId } = req.params;
410
+ const { agentId } = req.query;
411
+ if (!agentId) {
412
+ return res.status(400).json({ error: 'agentId is required' });
215
413
  }
414
+ const result = await this.getThreadExecutions(agentId, threadId);
415
+ res.json(result);
416
+ }
417
+ catch (error) {
418
+ console.error(chalk_1.default.red(`โŒ Error fetching executions: ${error.message}`));
419
+ res.status(500).json({ error: error.message });
216
420
  }
217
421
  });
218
- this.server.on('error', (error) => {
219
- console.error(chalk_1.default.red(`Failed to start server: ${error.message}`));
422
+ this.app.get('/api/v1/observability/executions/:executionId', async (req, res) => {
423
+ try {
424
+ const { executionId } = req.params;
425
+ const { agentId } = req.query;
426
+ if (!agentId) {
427
+ return res.status(400).json({ error: 'agentId is required' });
428
+ }
429
+ const result = await this.getExecutionTraces(agentId, executionId);
430
+ if (!result) {
431
+ return res.status(404).json({ error: 'Execution not found' });
432
+ }
433
+ res.json(result);
434
+ }
435
+ catch (error) {
436
+ console.error(chalk_1.default.red(`โŒ Error fetching execution: ${error.message}`));
437
+ res.status(500).json({ error: error.message });
438
+ }
220
439
  });
221
- await this.waitForServer(port, 10000);
222
440
  }
223
- async startFrontend(frontendPort, backendPort, verbose, spinner) {
224
- if (spinner) {
225
- spinner.text = 'Starting frontend...';
226
- }
227
- else {
228
- console.log('๐ŸŽจ Starting frontend...');
441
+ async getThreads(agentId, threadId, limit = 50) {
442
+ const threads = [];
443
+ const files = this.getAllTraceFiles();
444
+ for (const filePath of files) {
445
+ try {
446
+ const content = fs.readFileSync(filePath, 'utf-8');
447
+ const data = JSON.parse(content);
448
+ if (data.agentId !== agentId)
449
+ continue;
450
+ for (const [tid, threadData] of Object.entries(data.threads || {})) {
451
+ if (threadId && tid !== threadId)
452
+ continue;
453
+ const td = threadData;
454
+ const executions = td.executions || [];
455
+ const lastExecution = executions[executions.length - 1];
456
+ threads.push({
457
+ id: `thread-${tid}`,
458
+ executionId: lastExecution?.executionId,
459
+ threadId: tid,
460
+ tenantId: 'local',
461
+ agentId: data.agentId,
462
+ entityType: td.entityType || 'email',
463
+ entityValue: td.entityValue || tid,
464
+ userId: td.userId || null,
465
+ sessionId: data.sessionId,
466
+ channel: td.channel || 'sdk',
467
+ source: td.source || 'local',
468
+ inputMessage: lastExecution?.inputMessage,
469
+ outputMessage: lastExecution?.outputMessage,
470
+ totalTraces: executions.reduce((sum, e) => sum + (e.traces?.length || 0), 0),
471
+ totalDurationMs: executions.reduce((sum, e) => sum + (e.totalDurationMs || 0), 0),
472
+ totalTokens: executions.reduce((sum, e) => sum + (e.totalTokens || 0), 0),
473
+ totalCostUsd: executions.reduce((sum, e) => sum + (e.totalCostUsd || 0), 0),
474
+ status: lastExecution?.status || 'pending',
475
+ error: lastExecution?.error || null,
476
+ startedAt: executions[0]?.startedAt,
477
+ completedAt: lastExecution?.completedAt,
478
+ createdAt: executions[0]?.startedAt,
479
+ });
480
+ }
481
+ }
482
+ catch (error) {
483
+ }
229
484
  }
230
- const cliRootDir = path.join(__dirname, '..', '..', '..');
231
- const staticDir = path.join(cliRootDir, 'static');
232
- if (!fs.existsSync(staticDir)) {
233
- throw new Error('Static frontend files not found. Please ensure the CLI is properly installed.');
485
+ const sdkThreads = this.getSDKTraces(agentId);
486
+ const filteredSDKThreads = threadId
487
+ ? sdkThreads.filter(t => t.threadId === threadId)
488
+ : sdkThreads;
489
+ threads.push(...filteredSDKThreads);
490
+ return threads
491
+ .sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime())
492
+ .slice(0, limit);
493
+ }
494
+ async getThreadExecutions(agentId, threadId) {
495
+ const executions = [];
496
+ const files = this.getAllTraceFiles();
497
+ for (const filePath of files) {
498
+ try {
499
+ const content = fs.readFileSync(filePath, 'utf-8');
500
+ const data = JSON.parse(content);
501
+ if (data.agentId !== agentId)
502
+ continue;
503
+ const threadData = data.threads?.[threadId];
504
+ if (threadData?.executions) {
505
+ const formattedExecutions = threadData.executions.map((e) => ({
506
+ id: e.executionId,
507
+ executionId: e.executionId,
508
+ threadId: threadId,
509
+ tenantId: 'local',
510
+ agentId: data.agentId,
511
+ entityType: threadData.entityType || 'email',
512
+ entityValue: threadData.entityValue || threadId,
513
+ userId: threadData.userId || null,
514
+ sessionId: data.sessionId,
515
+ channel: threadData.channel || 'sdk',
516
+ source: threadData.source || 'local',
517
+ inputMessage: e.inputMessage,
518
+ outputMessage: e.outputMessage,
519
+ totalTraces: e.traces?.length || 0,
520
+ totalDurationMs: e.totalDurationMs || 0,
521
+ totalTokens: e.totalTokens || 0,
522
+ totalCostUsd: e.totalCostUsd || 0,
523
+ status: e.status || 'pending',
524
+ error: e.error || null,
525
+ startedAt: e.startedAt,
526
+ completedAt: e.completedAt,
527
+ createdAt: e.startedAt,
528
+ }));
529
+ executions.push(...formattedExecutions);
530
+ }
531
+ }
532
+ catch (error) {
533
+ }
234
534
  }
235
- const runflowDir = path.join(process.cwd(), '.runflow');
236
- const frontendDir = path.join(runflowDir, 'test-frontend');
237
- if (!fs.existsSync(frontendDir)) {
238
- fs.mkdirSync(frontendDir, { recursive: true });
535
+ const sdkThreads = this.getSDKTraces(agentId);
536
+ const sdkThread = sdkThreads.find(t => t.threadId === threadId);
537
+ if (sdkThread && sdkThread._sdkExecutions) {
538
+ const formattedExecutions = sdkThread._sdkExecutions.map((e) => ({
539
+ id: e.executionId,
540
+ executionId: e.executionId,
541
+ threadId: threadId,
542
+ tenantId: 'local',
543
+ agentId: agentId,
544
+ entityType: sdkThread.entityType,
545
+ entityValue: sdkThread.entityValue,
546
+ userId: sdkThread.userId,
547
+ sessionId: e.executionId,
548
+ channel: 'sdk',
549
+ source: 'local',
550
+ inputMessage: e.inputMessage,
551
+ outputMessage: e.outputMessage,
552
+ totalTraces: e.traces?.length || 0,
553
+ totalDurationMs: e.totalDurationMs,
554
+ totalTokens: e.totalTokens,
555
+ totalCostUsd: e.totalCostUsd,
556
+ status: e.status,
557
+ error: e.error || null,
558
+ startedAt: e.startedAt,
559
+ completedAt: e.completedAt,
560
+ createdAt: e.startedAt,
561
+ }));
562
+ executions.push(...formattedExecutions);
239
563
  }
240
- const staticFiles = ['index.html', 'style.css', 'app.js'];
241
- for (const file of staticFiles) {
242
- const sourcePath = path.join(staticDir, file);
243
- const destPath = path.join(frontendDir, file);
244
- if (fs.existsSync(sourcePath)) {
245
- fs.copyFileSync(sourcePath, destPath);
564
+ const sorted = executions.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
565
+ return {
566
+ threadId,
567
+ executions: sorted,
568
+ total: sorted.length,
569
+ };
570
+ }
571
+ async getExecutionTraces(agentId, executionId) {
572
+ const files = this.getAllTraceFiles();
573
+ for (const filePath of files) {
574
+ try {
575
+ const content = fs.readFileSync(filePath, 'utf-8');
576
+ const data = JSON.parse(content);
577
+ if (data.agentId !== agentId)
578
+ continue;
579
+ for (const [tid, threadData] of Object.entries(data.threads || {})) {
580
+ const td = threadData;
581
+ const execution = td.executions?.find((e) => e.executionId === executionId);
582
+ if (execution) {
583
+ const formattedExecution = {
584
+ id: execution.executionId,
585
+ executionId: execution.executionId,
586
+ threadId: tid,
587
+ tenantId: 'local',
588
+ agentId: data.agentId,
589
+ entityType: td.entityType || 'email',
590
+ entityValue: td.entityValue || tid,
591
+ userId: td.userId || null,
592
+ sessionId: data.sessionId,
593
+ channel: td.channel || 'sdk',
594
+ source: td.source || 'local',
595
+ inputMessage: execution.inputMessage,
596
+ outputMessage: execution.outputMessage,
597
+ totalTraces: execution.traces?.length || 0,
598
+ totalDurationMs: execution.totalDurationMs || 0,
599
+ totalTokens: execution.totalTokens || 0,
600
+ totalCostUsd: execution.totalCostUsd || 0,
601
+ status: execution.status || 'pending',
602
+ error: execution.error || null,
603
+ startedAt: execution.startedAt,
604
+ completedAt: execution.completedAt,
605
+ createdAt: execution.startedAt,
606
+ };
607
+ const traces = execution.traces || [];
608
+ const formattedTraces = traces.map((t) => ({
609
+ id: t.id || t.traceId,
610
+ traceId: t.id || t.traceId,
611
+ parentTraceId: t.parentId || null,
612
+ executionId: execution.executionId,
613
+ tenantId: 'local',
614
+ agentId: data.agentId,
615
+ threadId: tid,
616
+ userId: td.userId || null,
617
+ sessionId: data.sessionId,
618
+ type: t.type,
619
+ operation: t.name || t.type,
620
+ status: t.status,
621
+ error: t.error || null,
622
+ input: t.input || null,
623
+ output: t.output || null,
624
+ metadata: t.metadata || {},
625
+ startedAt: t.startedAt,
626
+ completedAt: t.completedAt,
627
+ durationMs: t.durationMs,
628
+ tokensTotal: t.metadata?.totalTokens || t.metadata?.tokens || null,
629
+ costUsd: t.metadata?.cost || null,
630
+ createdAt: t.startedAt,
631
+ children: t.children || [],
632
+ }));
633
+ return {
634
+ execution: formattedExecution,
635
+ traces: formattedTraces,
636
+ };
637
+ }
638
+ }
639
+ }
640
+ catch (error) {
246
641
  }
247
642
  }
248
- const frontendTemplatePath = path.join(cliRootDir, 'static', 'frontend-server-template.js');
249
- if (!fs.existsSync(frontendTemplatePath)) {
250
- throw new Error('Frontend server template not found. Please ensure the CLI is properly installed.');
643
+ const sdkThreads = this.getSDKTraces(agentId);
644
+ for (const thread of sdkThreads) {
645
+ if (thread._sdkExecutions) {
646
+ const execution = thread._sdkExecutions.find((e) => e.executionId === executionId);
647
+ if (execution) {
648
+ const formattedExecution = {
649
+ id: execution.executionId,
650
+ executionId: execution.executionId,
651
+ threadId: thread.threadId,
652
+ tenantId: 'local',
653
+ agentId: agentId,
654
+ entityType: thread.entityType,
655
+ entityValue: thread.entityValue,
656
+ userId: thread.userId,
657
+ sessionId: execution.executionId,
658
+ channel: 'sdk',
659
+ source: 'local',
660
+ inputMessage: execution.inputMessage,
661
+ outputMessage: execution.outputMessage,
662
+ totalTraces: this.countAllTraces(execution.traces),
663
+ totalDurationMs: execution.totalDurationMs || 0,
664
+ totalTokens: execution.totalTokens || 0,
665
+ totalCostUsd: execution.totalCostUsd || 0,
666
+ status: execution.status || 'success',
667
+ error: execution.error || null,
668
+ startedAt: execution.startedAt,
669
+ completedAt: execution.completedAt,
670
+ createdAt: execution.startedAt,
671
+ };
672
+ const formattedTraces = execution.traces || [];
673
+ return {
674
+ execution: formattedExecution,
675
+ traces: formattedTraces,
676
+ };
677
+ }
678
+ }
251
679
  }
252
- this.frontend = (0, child_process_1.spawn)('node', [frontendTemplatePath], {
253
- stdio: 'pipe',
254
- env: {
255
- ...process.env,
256
- NODE_ENV: 'development',
257
- RUNFLOW_FRONTEND_PORT: frontendPort.toString(),
258
- RUNFLOW_STATIC_DIR: frontendDir,
259
- },
260
- });
261
- this.frontend.stdout?.on('data', (data) => {
262
- if (verbose) {
263
- process.stdout.write(data.toString());
680
+ return null;
681
+ }
682
+ countAllTraces(traces) {
683
+ let count = traces.length;
684
+ traces.forEach((trace) => {
685
+ if (trace.children && trace.children.length > 0) {
686
+ count += this.countAllTraces(trace.children);
264
687
  }
265
688
  });
266
- this.frontend.stderr?.on('data', (data) => {
267
- if (verbose) {
268
- process.stderr.write(data.toString());
689
+ return count;
690
+ }
691
+ getAllTraceFiles() {
692
+ const files = [];
693
+ if (!fs.existsSync(this.storageDir)) {
694
+ return files;
695
+ }
696
+ const dateDirs = fs.readdirSync(this.storageDir);
697
+ for (const dateDir of dateDirs) {
698
+ const datePath = path.join(this.storageDir, dateDir);
699
+ if (!fs.statSync(datePath).isDirectory())
700
+ continue;
701
+ const traceFiles = fs.readdirSync(datePath).filter((f) => f.endsWith('.json'));
702
+ for (const file of traceFiles) {
703
+ files.push(path.join(datePath, file));
269
704
  }
270
- });
271
- this.frontend.on('error', (error) => {
272
- console.error(chalk_1.default.red(`Failed to start frontend: ${error.message}`));
273
- });
274
- await this.waitForServer(frontendPort, 5000);
705
+ }
706
+ return files;
275
707
  }
276
- async waitForServer(port, timeout) {
277
- const start = Date.now();
278
- while (Date.now() - start < timeout) {
279
- try {
280
- const response = await fetch(`http://localhost:${port}/api/health`);
281
- if (response.ok) {
282
- return;
708
+ getSDKTraces(agentId) {
709
+ const sdkTracesFile = path.join(process.cwd(), '.runflow', 'traces.json');
710
+ if (!fs.existsSync(sdkTracesFile)) {
711
+ return [];
712
+ }
713
+ try {
714
+ const content = fs.readFileSync(sdkTracesFile, 'utf-8');
715
+ const data = JSON.parse(content);
716
+ const threads = [];
717
+ const executionsMap = data.executions || {};
718
+ const threadGroups = {};
719
+ for (const [execId, execution] of Object.entries(executionsMap)) {
720
+ const exec = execution;
721
+ const threadId = exec.threadId || 'unknown';
722
+ if (!threadGroups[threadId]) {
723
+ threadGroups[threadId] = {
724
+ threadId: threadId,
725
+ entityType: exec.entityType || 'session',
726
+ entityValue: exec.entityValue || null,
727
+ userId: exec.userId || null,
728
+ channel: 'sdk',
729
+ source: 'local',
730
+ executions: [],
731
+ };
283
732
  }
733
+ const traces = exec.traces || [];
734
+ const agentTrace = traces.find((t) => t.type === 'agent_execution' &&
735
+ !t.parentTraceId &&
736
+ t.operation === 'agent_execution') || traces.find((t) => t.type === 'agent_execution' &&
737
+ !t.parentTraceId) || traces.find((t) => t.type === 'agent_execution');
738
+ const tracesWithChildren = this.buildTraceHierarchy(traces);
739
+ const executionData = {
740
+ executionId: exec.executionId,
741
+ inputMessage: agentTrace?.input?.message || 'N/A',
742
+ outputMessage: agentTrace?.output?.message || 'N/A',
743
+ status: exec.status || 'success',
744
+ totalDurationMs: traces.reduce((sum, t) => sum + (t.duration || 0), 0),
745
+ totalTokens: traces.reduce((sum, t) => sum + (t.metadata?.totalTokens || 0), 0),
746
+ totalCostUsd: 0,
747
+ startedAt: exec.startedAt || agentTrace?.startTime,
748
+ completedAt: agentTrace?.endTime || exec.completedAt,
749
+ traces: tracesWithChildren,
750
+ };
751
+ threadGroups[threadId].executions.push(executionData);
284
752
  }
285
- catch (error) {
753
+ for (const [threadId, threadData] of Object.entries(threadGroups)) {
754
+ const td = threadData;
755
+ const lastExecution = td.executions[td.executions.length - 1];
756
+ threads.push({
757
+ id: `thread-${threadId}`,
758
+ executionId: lastExecution?.executionId,
759
+ threadId: threadId,
760
+ tenantId: 'local',
761
+ agentId: agentId,
762
+ entityType: td.entityType,
763
+ entityValue: td.entityValue,
764
+ userId: td.userId,
765
+ sessionId: lastExecution?.executionId,
766
+ channel: td.channel,
767
+ source: td.source,
768
+ inputMessage: lastExecution?.inputMessage,
769
+ outputMessage: lastExecution?.outputMessage,
770
+ totalTraces: td.executions.reduce((sum, e) => sum + (e.traces?.length || 0), 0),
771
+ totalDurationMs: td.executions.reduce((sum, e) => sum + (e.totalDurationMs || 0), 0),
772
+ totalTokens: td.executions.reduce((sum, e) => sum + (e.totalTokens || 0), 0),
773
+ totalCostUsd: td.executions.reduce((sum, e) => sum + (e.totalCostUsd || 0), 0),
774
+ status: lastExecution?.status || 'success',
775
+ error: lastExecution?.error || null,
776
+ startedAt: td.executions[0]?.startedAt,
777
+ completedAt: lastExecution?.completedAt,
778
+ createdAt: td.executions[0]?.startedAt,
779
+ _sdkExecutions: td.executions,
780
+ });
286
781
  }
287
- await new Promise((resolve) => setTimeout(resolve, 500));
782
+ return threads;
783
+ }
784
+ catch (error) {
785
+ console.error(chalk_1.default.yellow('โš ๏ธ Failed to read SDK traces.json:'), error.message);
786
+ return [];
288
787
  }
289
- throw new Error(`Server on port ${port} failed to start`);
290
788
  }
291
- async runCommand(command, args, options = {}) {
292
- return new Promise((resolve, reject) => {
293
- const child = (0, child_process_1.spawn)(command, args, options);
294
- child.on('close', (code) => {
295
- if (code === 0) {
296
- resolve();
789
+ buildTraceHierarchy(traces) {
790
+ const traceMap = new Map();
791
+ traces.forEach((t) => {
792
+ traceMap.set(t.traceId, {
793
+ id: t.traceId,
794
+ traceId: t.traceId,
795
+ parentTraceId: t.parentTraceId || null,
796
+ type: t.type,
797
+ operation: t.operation,
798
+ status: t.status,
799
+ startedAt: t.startTime,
800
+ completedAt: t.endTime,
801
+ durationMs: t.duration,
802
+ input: t.input,
803
+ output: t.output,
804
+ metadata: t.metadata,
805
+ children: [],
806
+ });
807
+ });
808
+ const rootTraces = [];
809
+ traces.forEach((t) => {
810
+ const node = traceMap.get(t.traceId);
811
+ if (t.parentTraceId) {
812
+ const parent = traceMap.get(t.parentTraceId);
813
+ if (parent) {
814
+ parent.children.push(node);
297
815
  }
298
816
  else {
299
- reject(new Error(`Command failed with code ${code}`));
817
+ rootTraces.push(node);
300
818
  }
301
- });
302
- child.on('error', reject);
819
+ }
820
+ else {
821
+ rootTraces.push(node);
822
+ }
303
823
  });
824
+ return rootTraces;
304
825
  }
305
826
  openBrowser(url) {
827
+ const { spawn } = require('child_process');
306
828
  const start = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
307
829
  const spawnOptions = { detached: true, stdio: 'ignore' };
308
830
  if (process.platform === 'win32') {
309
831
  spawnOptions.shell = true;
310
832
  }
311
- (0, child_process_1.spawn)(start, [url], spawnOptions);
833
+ spawn(start, [url], spawnOptions);
312
834
  }
313
835
  setupCleanupHandlers() {
314
836
  const cleanup = async () => {
315
- console.log(chalk_1.default.yellow('\n๐Ÿ›‘ Shutting down test environment...'));
837
+ console.log(chalk_1.default.yellow('\n๐Ÿ›‘ Shutting down server...'));
316
838
  await this.cleanup();
317
839
  process.exit(0);
318
840
  };
319
841
  process.on('SIGINT', cleanup);
320
842
  process.on('SIGTERM', cleanup);
321
- process.on('exit', () => this.cleanup());
322
843
  }
323
844
  async cleanup() {
324
845
  if (this.server) {
325
- this.server.kill('SIGTERM');
846
+ this.server.close(() => {
847
+ console.log(chalk_1.default.gray('โœ“ Server stopped'));
848
+ });
326
849
  this.server = null;
327
850
  }
328
- if (this.frontend) {
329
- this.frontend.kill('SIGTERM');
330
- this.frontend = null;
331
- }
332
- const runflowDir = path.join(process.cwd(), '.runflow');
333
- const frontendDir = path.join(runflowDir, 'test-frontend');
334
- if (fs.existsSync(frontendDir)) {
335
- try {
336
- fs.rmSync(frontendDir, { recursive: true, force: true });
337
- console.log('๐Ÿงน Cleaned up temporary files');
338
- }
339
- catch (error) {
340
- }
341
- }
342
851
  }
343
852
  async keepAlive() {
344
853
  return new Promise(() => {
@@ -349,7 +858,7 @@ exports.TestCommand = TestCommand;
349
858
  __decorate([
350
859
  (0, nest_commander_1.Option)({
351
860
  flags: '-p, --port <port>',
352
- description: 'Port for the test server (default: 8547)',
861
+ description: 'Base port for the server (default: random between 3000-4000)',
353
862
  }),
354
863
  __metadata("design:type", Function),
355
864
  __metadata("design:paramtypes", [String]),
@@ -357,36 +866,18 @@ __decorate([
357
866
  ], TestCommand.prototype, "parsePort", null);
358
867
  __decorate([
359
868
  (0, nest_commander_1.Option)({
360
- flags: '-h, --host <host>',
361
- description: 'Host for the server (default: localhost)',
362
- }),
363
- __metadata("design:type", Function),
364
- __metadata("design:paramtypes", [String]),
365
- __metadata("design:returntype", String)
366
- ], TestCommand.prototype, "parseHost", null);
367
- __decorate([
368
- (0, nest_commander_1.Option)({
369
- flags: '--no-open',
869
+ flags: '--no-browser',
370
870
  description: "Don't open browser automatically",
371
871
  }),
372
872
  __metadata("design:type", Function),
373
873
  __metadata("design:paramtypes", []),
374
874
  __metadata("design:returntype", Boolean)
375
- ], TestCommand.prototype, "parseOpen", null);
376
- __decorate([
377
- (0, nest_commander_1.Option)({
378
- flags: '--verbose',
379
- description: 'Show detailed logs',
380
- }),
381
- __metadata("design:type", Function),
382
- __metadata("design:paramtypes", [String]),
383
- __metadata("design:returntype", Boolean)
384
- ], TestCommand.prototype, "parseVerbose", null);
875
+ ], TestCommand.prototype, "parseNoBrowser", null);
385
876
  exports.TestCommand = TestCommand = __decorate([
386
877
  (0, common_1.Injectable)(),
387
878
  (0, nest_commander_1.Command)({
388
879
  name: 'test',
389
- description: '๐Ÿงช Start local test server for Runflow agents with live reload',
880
+ description: '๐Ÿงช Start local observability server and open test portal',
390
881
  })
391
882
  ], TestCommand);
392
883
  //# sourceMappingURL=test.command.js.map