@runflow-ai/cli 0.2.10 โ†’ 0.2.12

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,722 @@ 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 agent;
172
+ if (agentFile.endsWith('.ts')) {
173
+ require('tsx/cjs');
174
+ const agentModule = require(agentFile);
175
+ agent = agentModule.default || agentModule.agent || agentModule;
176
+ if (typeof agent === 'function') {
177
+ agent = agent();
178
+ }
179
+ if (!agent || typeof agent.process !== 'function') {
180
+ const exportedKeys = Object.keys(agentModule);
181
+ for (const key of exportedKeys) {
182
+ const exported = agentModule[key];
183
+ if (typeof exported === 'function') {
184
+ const result = exported();
185
+ if (result && typeof result.process === 'function') {
186
+ agent = result;
187
+ break;
188
+ }
189
+ }
190
+ }
191
+ }
166
192
  }
167
193
  else {
168
- console.log('๐Ÿ“ฆ Installing dependencies...');
194
+ const agentModule = require(agentFile);
195
+ agent = agentModule.default || agentModule.agent || agentModule;
196
+ if (typeof agent === 'function') {
197
+ agent = agent();
198
+ }
199
+ if (!agent || typeof agent.process !== 'function') {
200
+ const exportedKeys = Object.keys(agentModule);
201
+ for (const key of exportedKeys) {
202
+ const exported = agentModule[key];
203
+ if (typeof exported === 'function') {
204
+ const result = exported();
205
+ if (result && typeof result.process === 'function') {
206
+ agent = result;
207
+ break;
208
+ }
209
+ }
210
+ }
211
+ }
169
212
  }
170
- await this.runCommand('npm', ['install'], { stdio: 'pipe' });
213
+ if (!agent || typeof agent.process !== 'function') {
214
+ throw new Error('Agent must export an agent instance with a process() method');
215
+ }
216
+ const config = this.loadRunflowConfig();
217
+ const input = {
218
+ message,
219
+ sessionId,
220
+ userId,
221
+ channel: 'local-test',
222
+ threadId,
223
+ metadata: {
224
+ timestamp: new Date().toISOString(),
225
+ source: 'local-cli',
226
+ agentId: config?.agentId,
227
+ }
228
+ };
229
+ const result = await agent.process(input);
230
+ await new Promise((resolve) => setTimeout(resolve, 3000));
231
+ const tracesFile = path.join(process.cwd(), '.runflow', 'traces.json');
232
+ if (fs.existsSync(tracesFile)) {
233
+ try {
234
+ const content = fs.readFileSync(tracesFile, 'utf-8');
235
+ const data = JSON.parse(content);
236
+ const execId = result.executionId || `exec-${Date.now()}`;
237
+ const execution = data.executions?.[execId];
238
+ if (execution) {
239
+ console.log(chalk_1.default.gray(` Traces saved: ${execution.traces?.length || 0}`));
240
+ const traceTypes = execution.traces?.map((t) => t.type) || [];
241
+ console.log(chalk_1.default.gray(` Types: ${[...new Set(traceTypes)].join(', ')}`));
242
+ }
243
+ }
244
+ catch (error) {
245
+ }
246
+ }
247
+ return {
248
+ executionId: result.executionId || `exec-${Date.now()}`,
249
+ message: result.output || result.message || result,
250
+ metadata: result.metadata || {},
251
+ };
252
+ }
253
+ catch (error) {
254
+ throw new Error(`Failed to execute agent: ${error.message}`);
171
255
  }
172
256
  }
173
- async startTestServer(port, mainFile, verbose, spinner) {
174
- if (spinner) {
175
- spinner.text = 'Starting test server...';
257
+ getRandomPort() {
258
+ return Math.floor(Math.random() * 1000) + 3000;
259
+ }
260
+ generateSessionId(req, bodySessionId) {
261
+ if (bodySessionId) {
262
+ return bodySessionId;
176
263
  }
177
- else {
178
- console.log('๐Ÿš€ Starting test server...');
264
+ const headerSessionId = req.headers['x-session-id'];
265
+ if (headerSessionId) {
266
+ return headerSessionId;
179
267
  }
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.');
268
+ const cookieHeader = req.headers['cookie'];
269
+ if (cookieHeader) {
270
+ const cookies = cookieHeader.split(';').reduce((acc, cookie) => {
271
+ const [key, value] = cookie.trim().split('=');
272
+ acc[key] = value;
273
+ return acc;
274
+ }, {});
275
+ if (cookies['runflow_session']) {
276
+ return cookies['runflow_session'];
277
+ }
184
278
  }
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'));
279
+ const fingerprint = this.createFingerprint(req);
280
+ return `auto_${fingerprint}_${Date.now()}`;
281
+ }
282
+ generateUserId(req, sessionId) {
283
+ const cookieHeader = req.headers['cookie'];
284
+ if (cookieHeader) {
285
+ const cookies = cookieHeader.split(';').reduce((acc, cookie) => {
286
+ const [key, value] = cookie.trim().split('=');
287
+ acc[key] = value;
288
+ return acc;
289
+ }, {});
290
+ if (cookies['runflow_user']) {
291
+ return cookies['runflow_user'];
292
+ }
199
293
  }
200
- this.server.stdout?.on('data', (data) => {
201
- const output = data.toString();
202
- if (verbose) {
203
- process.stdout.write(`[SERVER] ${output}`);
294
+ return sessionId;
295
+ }
296
+ createFingerprint(req) {
297
+ const components = [
298
+ req.headers['user-agent'] || '',
299
+ req.headers['accept-language'] || '',
300
+ req.ip || req.socket.remoteAddress || '',
301
+ ].join('|');
302
+ return (0, crypto_1.createHash)('md5').update(components).digest('hex').substring(0, 12);
303
+ }
304
+ ensureStorageDir() {
305
+ if (!fs.existsSync(this.storageDir)) {
306
+ fs.mkdirSync(this.storageDir, { recursive: true });
307
+ }
308
+ }
309
+ async startObservabilityServer(port) {
310
+ return new Promise((resolve, reject) => {
311
+ this.app = (0, express_1.default)();
312
+ this.app.use((0, cors_1.default)({
313
+ origin: true,
314
+ credentials: true,
315
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
316
+ allowedHeaders: ['Content-Type', 'X-Thread-Id', 'X-Session-Id', 'Authorization', 'Cookie'],
317
+ exposedHeaders: ['Content-Length', 'X-Request-Id', 'X-Session-ID', 'X-User-ID', 'X-Timestamp'],
318
+ maxAge: 86400,
319
+ }));
320
+ this.app.use(express_1.default.json());
321
+ const distPath = path.join(__dirname, '..', '..', '..', 'static', 'dist-test');
322
+ this.app.use('/assets', express_1.default.static(path.join(distPath, 'assets')));
323
+ this.app.use('/widget', express_1.default.static(path.join(distPath, 'widget')));
324
+ this.app.get('/health', (req, res) => {
325
+ res.status(200).json({ status: 'ok', mode: 'local', version: '1.0.0' });
326
+ });
327
+ this.setupObservabilityEndpoints();
328
+ this.app.get('*', (req, res) => {
329
+ const indexPath = path.join(distPath, 'index-test.html');
330
+ res.sendFile(indexPath);
331
+ });
332
+ this.server = this.app.listen(port, () => {
333
+ resolve();
334
+ });
335
+ this.server.on('error', (error) => {
336
+ if (error.code === 'EADDRINUSE') {
337
+ reject(new Error(`Port ${port} is already in use`));
338
+ }
339
+ else {
340
+ reject(error);
341
+ }
342
+ });
343
+ });
344
+ }
345
+ setupObservabilityEndpoints() {
346
+ if (!this.app)
347
+ return;
348
+ this.app.post('/api/v1/agents/execute', async (req, res) => {
349
+ try {
350
+ const { message, threadId, sessionId: bodySessionId } = req.body;
351
+ if (!message) {
352
+ return res.status(400).json({
353
+ success: false,
354
+ error: 'message is required'
355
+ });
356
+ }
357
+ const normalizedMessage = message.toLowerCase().trim();
358
+ if (normalizedMessage === 'ping' || normalizedMessage === 'pong') {
359
+ return res.json({
360
+ success: true,
361
+ executionId: `ping-${Date.now()}`,
362
+ message: 'pong',
363
+ sessionId: 'ping-session',
364
+ userId: 'ping-user',
365
+ metadata: { isPing: true },
366
+ });
367
+ }
368
+ const sessionId = this.generateSessionId(req, bodySessionId);
369
+ const userId = this.generateUserId(req, sessionId);
370
+ console.log(chalk_1.default.gray(`๐Ÿ“จ [${new Date().toISOString()}] Executing agent...`));
371
+ console.log(chalk_1.default.gray(` Message: ${message.substring(0, 50)}${message.length > 50 ? '...' : ''}`));
372
+ if (threadId) {
373
+ console.log(chalk_1.default.gray(` Thread: ${threadId}`));
374
+ }
375
+ console.log(chalk_1.default.gray(` Session: ${sessionId}`));
376
+ const result = await this.executeAgent(message, threadId || sessionId, sessionId, userId);
377
+ console.log(chalk_1.default.green(`โœ“ [${new Date().toISOString()}] Agent executed successfully`));
378
+ console.log(chalk_1.default.gray(` Execution ID: ${result.executionId}`));
379
+ console.log('');
380
+ res.setHeader('X-Session-ID', sessionId);
381
+ res.setHeader('X-User-ID', userId);
382
+ res.setHeader('X-Timestamp', new Date().toISOString());
383
+ res.setHeader('Set-Cookie', [
384
+ `runflow_session=${sessionId}; Path=/; HttpOnly; SameSite=Lax`,
385
+ `runflow_user=${userId}; Path=/; HttpOnly; SameSite=Lax`,
386
+ ]);
387
+ res.json({
388
+ success: true,
389
+ executionId: result.executionId,
390
+ message: result.message,
391
+ sessionId: sessionId,
392
+ userId: userId,
393
+ metadata: result.metadata,
394
+ });
395
+ }
396
+ catch (error) {
397
+ console.error(chalk_1.default.red(`โŒ [${new Date().toISOString()}] Agent execution failed:`));
398
+ console.error(chalk_1.default.red(` ${error.message}`));
399
+ console.log('');
400
+ res.status(500).json({
401
+ success: false,
402
+ error: error.message
403
+ });
204
404
  }
205
405
  });
206
- this.server.stderr?.on('data', (data) => {
207
- const error = data.toString();
208
- if (verbose) {
209
- process.stderr.write(`[SERVER-ERR] ${error}`);
406
+ this.app.get('/api/v1/observability/threads', async (req, res) => {
407
+ try {
408
+ const { agentId, threadId, limit = 50 } = req.query;
409
+ if (!agentId) {
410
+ return res.status(400).json({ error: 'agentId is required' });
411
+ }
412
+ const threads = await this.getThreads(agentId, threadId, parseInt(limit));
413
+ res.json({ threads, total: threads.length });
210
414
  }
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}`));
415
+ catch (error) {
416
+ console.error(chalk_1.default.red(`โŒ Error fetching threads: ${error.message}`));
417
+ res.status(500).json({ error: error.message });
418
+ }
419
+ });
420
+ this.app.get('/api/v1/observability/threads/:threadId/executions', async (req, res) => {
421
+ try {
422
+ const { threadId } = req.params;
423
+ const { agentId } = req.query;
424
+ if (!agentId) {
425
+ return res.status(400).json({ error: 'agentId is required' });
215
426
  }
427
+ const result = await this.getThreadExecutions(agentId, threadId);
428
+ res.json(result);
429
+ }
430
+ catch (error) {
431
+ console.error(chalk_1.default.red(`โŒ Error fetching executions: ${error.message}`));
432
+ res.status(500).json({ error: error.message });
216
433
  }
217
434
  });
218
- this.server.on('error', (error) => {
219
- console.error(chalk_1.default.red(`Failed to start server: ${error.message}`));
435
+ this.app.get('/api/v1/observability/executions/:executionId', async (req, res) => {
436
+ try {
437
+ const { executionId } = req.params;
438
+ const { agentId } = req.query;
439
+ if (!agentId) {
440
+ return res.status(400).json({ error: 'agentId is required' });
441
+ }
442
+ const result = await this.getExecutionTraces(agentId, executionId);
443
+ if (!result) {
444
+ return res.status(404).json({ error: 'Execution not found' });
445
+ }
446
+ res.json(result);
447
+ }
448
+ catch (error) {
449
+ console.error(chalk_1.default.red(`โŒ Error fetching execution: ${error.message}`));
450
+ res.status(500).json({ error: error.message });
451
+ }
220
452
  });
221
- await this.waitForServer(port, 10000);
222
453
  }
223
- async startFrontend(frontendPort, backendPort, verbose, spinner) {
224
- if (spinner) {
225
- spinner.text = 'Starting frontend...';
226
- }
227
- else {
228
- console.log('๐ŸŽจ Starting frontend...');
454
+ async getThreads(agentId, threadId, limit = 50) {
455
+ const threads = [];
456
+ const files = this.getAllTraceFiles();
457
+ for (const filePath of files) {
458
+ try {
459
+ const content = fs.readFileSync(filePath, 'utf-8');
460
+ const data = JSON.parse(content);
461
+ if (data.agentId !== agentId)
462
+ continue;
463
+ for (const [tid, threadData] of Object.entries(data.threads || {})) {
464
+ if (threadId && tid !== threadId)
465
+ continue;
466
+ const td = threadData;
467
+ const executions = td.executions || [];
468
+ const lastExecution = executions[executions.length - 1];
469
+ threads.push({
470
+ id: `thread-${tid}`,
471
+ executionId: lastExecution?.executionId,
472
+ threadId: tid,
473
+ tenantId: 'local',
474
+ agentId: data.agentId,
475
+ entityType: td.entityType || 'email',
476
+ entityValue: td.entityValue || tid,
477
+ userId: td.userId || null,
478
+ sessionId: data.sessionId,
479
+ channel: td.channel || 'sdk',
480
+ source: td.source || 'local',
481
+ inputMessage: lastExecution?.inputMessage,
482
+ outputMessage: lastExecution?.outputMessage,
483
+ totalTraces: executions.reduce((sum, e) => sum + (e.traces?.length || 0), 0),
484
+ totalDurationMs: executions.reduce((sum, e) => sum + (e.totalDurationMs || 0), 0),
485
+ totalTokens: executions.reduce((sum, e) => sum + (e.totalTokens || 0), 0),
486
+ totalCostUsd: executions.reduce((sum, e) => sum + (e.totalCostUsd || 0), 0),
487
+ status: lastExecution?.status || 'pending',
488
+ error: lastExecution?.error || null,
489
+ startedAt: executions[0]?.startedAt,
490
+ completedAt: lastExecution?.completedAt,
491
+ createdAt: executions[0]?.startedAt,
492
+ });
493
+ }
494
+ }
495
+ catch (error) {
496
+ }
229
497
  }
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.');
498
+ const sdkThreads = this.getSDKTraces(agentId);
499
+ const filteredSDKThreads = threadId
500
+ ? sdkThreads.filter(t => t.threadId === threadId)
501
+ : sdkThreads;
502
+ threads.push(...filteredSDKThreads);
503
+ return threads
504
+ .sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime())
505
+ .slice(0, limit);
506
+ }
507
+ async getThreadExecutions(agentId, threadId) {
508
+ const executions = [];
509
+ const files = this.getAllTraceFiles();
510
+ for (const filePath of files) {
511
+ try {
512
+ const content = fs.readFileSync(filePath, 'utf-8');
513
+ const data = JSON.parse(content);
514
+ if (data.agentId !== agentId)
515
+ continue;
516
+ const threadData = data.threads?.[threadId];
517
+ if (threadData?.executions) {
518
+ const formattedExecutions = threadData.executions.map((e) => ({
519
+ id: e.executionId,
520
+ executionId: e.executionId,
521
+ threadId: threadId,
522
+ tenantId: 'local',
523
+ agentId: data.agentId,
524
+ entityType: threadData.entityType || 'email',
525
+ entityValue: threadData.entityValue || threadId,
526
+ userId: threadData.userId || null,
527
+ sessionId: data.sessionId,
528
+ channel: threadData.channel || 'sdk',
529
+ source: threadData.source || 'local',
530
+ inputMessage: e.inputMessage,
531
+ outputMessage: e.outputMessage,
532
+ totalTraces: e.traces?.length || 0,
533
+ totalDurationMs: e.totalDurationMs || 0,
534
+ totalTokens: e.totalTokens || 0,
535
+ totalCostUsd: e.totalCostUsd || 0,
536
+ status: e.status || 'pending',
537
+ error: e.error || null,
538
+ startedAt: e.startedAt,
539
+ completedAt: e.completedAt,
540
+ createdAt: e.startedAt,
541
+ }));
542
+ executions.push(...formattedExecutions);
543
+ }
544
+ }
545
+ catch (error) {
546
+ }
234
547
  }
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 });
548
+ const sdkThreads = this.getSDKTraces(agentId);
549
+ const sdkThread = sdkThreads.find(t => t.threadId === threadId);
550
+ if (sdkThread && sdkThread._sdkExecutions) {
551
+ const formattedExecutions = sdkThread._sdkExecutions.map((e) => ({
552
+ id: e.executionId,
553
+ executionId: e.executionId,
554
+ threadId: threadId,
555
+ tenantId: 'local',
556
+ agentId: agentId,
557
+ entityType: sdkThread.entityType,
558
+ entityValue: sdkThread.entityValue,
559
+ userId: sdkThread.userId,
560
+ sessionId: e.executionId,
561
+ channel: 'sdk',
562
+ source: 'local',
563
+ inputMessage: e.inputMessage,
564
+ outputMessage: e.outputMessage,
565
+ totalTraces: e.traces?.length || 0,
566
+ totalDurationMs: e.totalDurationMs,
567
+ totalTokens: e.totalTokens,
568
+ totalCostUsd: e.totalCostUsd,
569
+ status: e.status,
570
+ error: e.error || null,
571
+ startedAt: e.startedAt,
572
+ completedAt: e.completedAt,
573
+ createdAt: e.startedAt,
574
+ }));
575
+ executions.push(...formattedExecutions);
239
576
  }
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);
577
+ const sorted = executions.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
578
+ return {
579
+ threadId,
580
+ executions: sorted,
581
+ total: sorted.length,
582
+ };
583
+ }
584
+ async getExecutionTraces(agentId, executionId) {
585
+ const files = this.getAllTraceFiles();
586
+ for (const filePath of files) {
587
+ try {
588
+ const content = fs.readFileSync(filePath, 'utf-8');
589
+ const data = JSON.parse(content);
590
+ if (data.agentId !== agentId)
591
+ continue;
592
+ for (const [tid, threadData] of Object.entries(data.threads || {})) {
593
+ const td = threadData;
594
+ const execution = td.executions?.find((e) => e.executionId === executionId);
595
+ if (execution) {
596
+ const formattedExecution = {
597
+ id: execution.executionId,
598
+ executionId: execution.executionId,
599
+ threadId: tid,
600
+ tenantId: 'local',
601
+ agentId: data.agentId,
602
+ entityType: td.entityType || 'email',
603
+ entityValue: td.entityValue || tid,
604
+ userId: td.userId || null,
605
+ sessionId: data.sessionId,
606
+ channel: td.channel || 'sdk',
607
+ source: td.source || 'local',
608
+ inputMessage: execution.inputMessage,
609
+ outputMessage: execution.outputMessage,
610
+ totalTraces: execution.traces?.length || 0,
611
+ totalDurationMs: execution.totalDurationMs || 0,
612
+ totalTokens: execution.totalTokens || 0,
613
+ totalCostUsd: execution.totalCostUsd || 0,
614
+ status: execution.status || 'pending',
615
+ error: execution.error || null,
616
+ startedAt: execution.startedAt,
617
+ completedAt: execution.completedAt,
618
+ createdAt: execution.startedAt,
619
+ };
620
+ const traces = execution.traces || [];
621
+ const formattedTraces = traces.map((t) => ({
622
+ id: t.id || t.traceId,
623
+ traceId: t.id || t.traceId,
624
+ parentTraceId: t.parentId || null,
625
+ executionId: execution.executionId,
626
+ tenantId: 'local',
627
+ agentId: data.agentId,
628
+ threadId: tid,
629
+ userId: td.userId || null,
630
+ sessionId: data.sessionId,
631
+ type: t.type,
632
+ operation: t.name || t.type,
633
+ status: t.status,
634
+ error: t.error || null,
635
+ input: t.input || null,
636
+ output: t.output || null,
637
+ metadata: t.metadata || {},
638
+ startedAt: t.startedAt,
639
+ completedAt: t.completedAt,
640
+ durationMs: t.durationMs,
641
+ tokensTotal: t.metadata?.totalTokens || t.metadata?.tokens || null,
642
+ costUsd: t.metadata?.cost || null,
643
+ createdAt: t.startedAt,
644
+ children: t.children || [],
645
+ }));
646
+ return {
647
+ execution: formattedExecution,
648
+ traces: formattedTraces,
649
+ };
650
+ }
651
+ }
652
+ }
653
+ catch (error) {
246
654
  }
247
655
  }
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.');
656
+ const sdkThreads = this.getSDKTraces(agentId);
657
+ for (const thread of sdkThreads) {
658
+ if (thread._sdkExecutions) {
659
+ const execution = thread._sdkExecutions.find((e) => e.executionId === executionId);
660
+ if (execution) {
661
+ const formattedExecution = {
662
+ id: execution.executionId,
663
+ executionId: execution.executionId,
664
+ threadId: thread.threadId,
665
+ tenantId: 'local',
666
+ agentId: agentId,
667
+ entityType: thread.entityType,
668
+ entityValue: thread.entityValue,
669
+ userId: thread.userId,
670
+ sessionId: execution.executionId,
671
+ channel: 'sdk',
672
+ source: 'local',
673
+ inputMessage: execution.inputMessage,
674
+ outputMessage: execution.outputMessage,
675
+ totalTraces: this.countAllTraces(execution.traces),
676
+ totalDurationMs: execution.totalDurationMs || 0,
677
+ totalTokens: execution.totalTokens || 0,
678
+ totalCostUsd: execution.totalCostUsd || 0,
679
+ status: execution.status || 'success',
680
+ error: execution.error || null,
681
+ startedAt: execution.startedAt,
682
+ completedAt: execution.completedAt,
683
+ createdAt: execution.startedAt,
684
+ };
685
+ const formattedTraces = execution.traces || [];
686
+ return {
687
+ execution: formattedExecution,
688
+ traces: formattedTraces,
689
+ };
690
+ }
691
+ }
251
692
  }
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());
693
+ return null;
694
+ }
695
+ countAllTraces(traces) {
696
+ let count = traces.length;
697
+ traces.forEach((trace) => {
698
+ if (trace.children && trace.children.length > 0) {
699
+ count += this.countAllTraces(trace.children);
264
700
  }
265
701
  });
266
- this.frontend.stderr?.on('data', (data) => {
267
- if (verbose) {
268
- process.stderr.write(data.toString());
702
+ return count;
703
+ }
704
+ getAllTraceFiles() {
705
+ const files = [];
706
+ if (!fs.existsSync(this.storageDir)) {
707
+ return files;
708
+ }
709
+ const dateDirs = fs.readdirSync(this.storageDir);
710
+ for (const dateDir of dateDirs) {
711
+ const datePath = path.join(this.storageDir, dateDir);
712
+ if (!fs.statSync(datePath).isDirectory())
713
+ continue;
714
+ const traceFiles = fs.readdirSync(datePath).filter((f) => f.endsWith('.json'));
715
+ for (const file of traceFiles) {
716
+ files.push(path.join(datePath, file));
269
717
  }
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);
718
+ }
719
+ return files;
275
720
  }
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;
721
+ getSDKTraces(agentId) {
722
+ const sdkTracesFile = path.join(process.cwd(), '.runflow', 'traces.json');
723
+ if (!fs.existsSync(sdkTracesFile)) {
724
+ return [];
725
+ }
726
+ try {
727
+ const content = fs.readFileSync(sdkTracesFile, 'utf-8');
728
+ const data = JSON.parse(content);
729
+ const threads = [];
730
+ const executionsMap = data.executions || {};
731
+ const threadGroups = {};
732
+ for (const [execId, execution] of Object.entries(executionsMap)) {
733
+ const exec = execution;
734
+ const threadId = exec.threadId || 'unknown';
735
+ if (!threadGroups[threadId]) {
736
+ threadGroups[threadId] = {
737
+ threadId: threadId,
738
+ entityType: exec.entityType || 'session',
739
+ entityValue: exec.entityValue || null,
740
+ userId: exec.userId || null,
741
+ channel: 'sdk',
742
+ source: 'local',
743
+ executions: [],
744
+ };
283
745
  }
746
+ const traces = exec.traces || [];
747
+ const agentTrace = traces.find((t) => t.type === 'agent_execution' &&
748
+ !t.parentTraceId &&
749
+ t.operation === 'agent_execution') || traces.find((t) => t.type === 'agent_execution' &&
750
+ !t.parentTraceId) || traces.find((t) => t.type === 'agent_execution');
751
+ const tracesWithChildren = this.buildTraceHierarchy(traces);
752
+ const executionData = {
753
+ executionId: exec.executionId,
754
+ inputMessage: agentTrace?.input?.message || 'N/A',
755
+ outputMessage: agentTrace?.output?.message || 'N/A',
756
+ status: exec.status || 'success',
757
+ totalDurationMs: traces.reduce((sum, t) => sum + (t.duration || 0), 0),
758
+ totalTokens: traces.reduce((sum, t) => sum + (t.metadata?.totalTokens || 0), 0),
759
+ totalCostUsd: 0,
760
+ startedAt: exec.startedAt || agentTrace?.startTime,
761
+ completedAt: agentTrace?.endTime || exec.completedAt,
762
+ traces: tracesWithChildren,
763
+ };
764
+ threadGroups[threadId].executions.push(executionData);
284
765
  }
285
- catch (error) {
766
+ for (const [threadId, threadData] of Object.entries(threadGroups)) {
767
+ const td = threadData;
768
+ const lastExecution = td.executions[td.executions.length - 1];
769
+ threads.push({
770
+ id: `thread-${threadId}`,
771
+ executionId: lastExecution?.executionId,
772
+ threadId: threadId,
773
+ tenantId: 'local',
774
+ agentId: agentId,
775
+ entityType: td.entityType,
776
+ entityValue: td.entityValue,
777
+ userId: td.userId,
778
+ sessionId: lastExecution?.executionId,
779
+ channel: td.channel,
780
+ source: td.source,
781
+ inputMessage: lastExecution?.inputMessage,
782
+ outputMessage: lastExecution?.outputMessage,
783
+ totalTraces: td.executions.reduce((sum, e) => sum + (e.traces?.length || 0), 0),
784
+ totalDurationMs: td.executions.reduce((sum, e) => sum + (e.totalDurationMs || 0), 0),
785
+ totalTokens: td.executions.reduce((sum, e) => sum + (e.totalTokens || 0), 0),
786
+ totalCostUsd: td.executions.reduce((sum, e) => sum + (e.totalCostUsd || 0), 0),
787
+ status: lastExecution?.status || 'success',
788
+ error: lastExecution?.error || null,
789
+ startedAt: td.executions[0]?.startedAt,
790
+ completedAt: lastExecution?.completedAt,
791
+ createdAt: td.executions[0]?.startedAt,
792
+ _sdkExecutions: td.executions,
793
+ });
286
794
  }
287
- await new Promise((resolve) => setTimeout(resolve, 500));
795
+ return threads;
796
+ }
797
+ catch (error) {
798
+ console.error(chalk_1.default.yellow('โš ๏ธ Failed to read SDK traces.json:'), error.message);
799
+ return [];
288
800
  }
289
- throw new Error(`Server on port ${port} failed to start`);
290
801
  }
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();
802
+ buildTraceHierarchy(traces) {
803
+ const traceMap = new Map();
804
+ traces.forEach((t) => {
805
+ traceMap.set(t.traceId, {
806
+ id: t.traceId,
807
+ traceId: t.traceId,
808
+ parentTraceId: t.parentTraceId || null,
809
+ type: t.type,
810
+ operation: t.operation,
811
+ status: t.status,
812
+ startedAt: t.startTime,
813
+ completedAt: t.endTime,
814
+ durationMs: t.duration,
815
+ input: t.input,
816
+ output: t.output,
817
+ metadata: t.metadata,
818
+ children: [],
819
+ });
820
+ });
821
+ const rootTraces = [];
822
+ traces.forEach((t) => {
823
+ const node = traceMap.get(t.traceId);
824
+ if (t.parentTraceId) {
825
+ const parent = traceMap.get(t.parentTraceId);
826
+ if (parent) {
827
+ parent.children.push(node);
297
828
  }
298
829
  else {
299
- reject(new Error(`Command failed with code ${code}`));
830
+ rootTraces.push(node);
300
831
  }
301
- });
302
- child.on('error', reject);
832
+ }
833
+ else {
834
+ rootTraces.push(node);
835
+ }
303
836
  });
837
+ return rootTraces;
304
838
  }
305
839
  openBrowser(url) {
840
+ const { spawn } = require('child_process');
306
841
  const start = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
307
842
  const spawnOptions = { detached: true, stdio: 'ignore' };
308
843
  if (process.platform === 'win32') {
309
844
  spawnOptions.shell = true;
310
845
  }
311
- (0, child_process_1.spawn)(start, [url], spawnOptions);
846
+ spawn(start, [url], spawnOptions);
312
847
  }
313
848
  setupCleanupHandlers() {
314
849
  const cleanup = async () => {
315
- console.log(chalk_1.default.yellow('\n๐Ÿ›‘ Shutting down test environment...'));
850
+ console.log(chalk_1.default.yellow('\n๐Ÿ›‘ Shutting down server...'));
316
851
  await this.cleanup();
317
852
  process.exit(0);
318
853
  };
319
854
  process.on('SIGINT', cleanup);
320
855
  process.on('SIGTERM', cleanup);
321
- process.on('exit', () => this.cleanup());
322
856
  }
323
857
  async cleanup() {
324
858
  if (this.server) {
325
- this.server.kill('SIGTERM');
859
+ this.server.close(() => {
860
+ console.log(chalk_1.default.gray('โœ“ Server stopped'));
861
+ });
326
862
  this.server = null;
327
863
  }
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
864
  }
343
865
  async keepAlive() {
344
866
  return new Promise(() => {
@@ -349,7 +871,7 @@ exports.TestCommand = TestCommand;
349
871
  __decorate([
350
872
  (0, nest_commander_1.Option)({
351
873
  flags: '-p, --port <port>',
352
- description: 'Port for the test server (default: 8547)',
874
+ description: 'Base port for the server (default: random between 3000-4000)',
353
875
  }),
354
876
  __metadata("design:type", Function),
355
877
  __metadata("design:paramtypes", [String]),
@@ -357,36 +879,18 @@ __decorate([
357
879
  ], TestCommand.prototype, "parsePort", null);
358
880
  __decorate([
359
881
  (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',
882
+ flags: '--no-browser',
370
883
  description: "Don't open browser automatically",
371
884
  }),
372
885
  __metadata("design:type", Function),
373
886
  __metadata("design:paramtypes", []),
374
887
  __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);
888
+ ], TestCommand.prototype, "parseNoBrowser", null);
385
889
  exports.TestCommand = TestCommand = __decorate([
386
890
  (0, common_1.Injectable)(),
387
891
  (0, nest_commander_1.Command)({
388
892
  name: 'test',
389
- description: '๐Ÿงช Start local test server for Runflow agents with live reload',
893
+ description: '๐Ÿงช Start local observability server and open test portal',
390
894
  })
391
895
  ], TestCommand);
392
896
  //# sourceMappingURL=test.command.js.map