@loxia-labs/loxia-autopilot-one 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +267 -0
  2. package/README.md +509 -0
  3. package/bin/cli.js +117 -0
  4. package/package.json +94 -0
  5. package/scripts/install-scanners.js +236 -0
  6. package/src/analyzers/CSSAnalyzer.js +297 -0
  7. package/src/analyzers/ConfigValidator.js +690 -0
  8. package/src/analyzers/ESLintAnalyzer.js +320 -0
  9. package/src/analyzers/JavaScriptAnalyzer.js +261 -0
  10. package/src/analyzers/PrettierFormatter.js +247 -0
  11. package/src/analyzers/PythonAnalyzer.js +266 -0
  12. package/src/analyzers/SecurityAnalyzer.js +729 -0
  13. package/src/analyzers/TypeScriptAnalyzer.js +247 -0
  14. package/src/analyzers/codeCloneDetector/analyzer.js +344 -0
  15. package/src/analyzers/codeCloneDetector/detector.js +203 -0
  16. package/src/analyzers/codeCloneDetector/index.js +160 -0
  17. package/src/analyzers/codeCloneDetector/parser.js +199 -0
  18. package/src/analyzers/codeCloneDetector/reporter.js +148 -0
  19. package/src/analyzers/codeCloneDetector/scanner.js +59 -0
  20. package/src/core/agentPool.js +1474 -0
  21. package/src/core/agentScheduler.js +2147 -0
  22. package/src/core/contextManager.js +709 -0
  23. package/src/core/messageProcessor.js +732 -0
  24. package/src/core/orchestrator.js +548 -0
  25. package/src/core/stateManager.js +877 -0
  26. package/src/index.js +631 -0
  27. package/src/interfaces/cli.js +549 -0
  28. package/src/interfaces/webServer.js +2162 -0
  29. package/src/modules/fileExplorer/controller.js +280 -0
  30. package/src/modules/fileExplorer/index.js +37 -0
  31. package/src/modules/fileExplorer/middleware.js +92 -0
  32. package/src/modules/fileExplorer/routes.js +125 -0
  33. package/src/modules/fileExplorer/types.js +44 -0
  34. package/src/services/aiService.js +1232 -0
  35. package/src/services/apiKeyManager.js +164 -0
  36. package/src/services/benchmarkService.js +366 -0
  37. package/src/services/budgetService.js +539 -0
  38. package/src/services/contextInjectionService.js +247 -0
  39. package/src/services/conversationCompactionService.js +637 -0
  40. package/src/services/errorHandler.js +810 -0
  41. package/src/services/fileAttachmentService.js +544 -0
  42. package/src/services/modelRouterService.js +366 -0
  43. package/src/services/modelsService.js +322 -0
  44. package/src/services/qualityInspector.js +796 -0
  45. package/src/services/tokenCountingService.js +536 -0
  46. package/src/tools/agentCommunicationTool.js +1344 -0
  47. package/src/tools/agentDelayTool.js +485 -0
  48. package/src/tools/asyncToolManager.js +604 -0
  49. package/src/tools/baseTool.js +800 -0
  50. package/src/tools/browserTool.js +920 -0
  51. package/src/tools/cloneDetectionTool.js +621 -0
  52. package/src/tools/dependencyResolverTool.js +1215 -0
  53. package/src/tools/fileContentReplaceTool.js +875 -0
  54. package/src/tools/fileSystemTool.js +1107 -0
  55. package/src/tools/fileTreeTool.js +853 -0
  56. package/src/tools/imageTool.js +901 -0
  57. package/src/tools/importAnalyzerTool.js +1060 -0
  58. package/src/tools/jobDoneTool.js +248 -0
  59. package/src/tools/seekTool.js +956 -0
  60. package/src/tools/staticAnalysisTool.js +1778 -0
  61. package/src/tools/taskManagerTool.js +2873 -0
  62. package/src/tools/terminalTool.js +2304 -0
  63. package/src/tools/webTool.js +1430 -0
  64. package/src/types/agent.js +519 -0
  65. package/src/types/contextReference.js +972 -0
  66. package/src/types/conversation.js +730 -0
  67. package/src/types/toolCommand.js +747 -0
  68. package/src/utilities/attachmentValidator.js +292 -0
  69. package/src/utilities/configManager.js +582 -0
  70. package/src/utilities/constants.js +722 -0
  71. package/src/utilities/directoryAccessManager.js +535 -0
  72. package/src/utilities/fileProcessor.js +307 -0
  73. package/src/utilities/logger.js +436 -0
  74. package/src/utilities/tagParser.js +1246 -0
  75. package/src/utilities/toolConstants.js +317 -0
  76. package/web-ui/build/index.html +15 -0
  77. package/web-ui/build/logo.png +0 -0
  78. package/web-ui/build/logo2.png +0 -0
  79. package/web-ui/build/static/index-CjkkcnFA.js +344 -0
  80. package/web-ui/build/static/index-Dy2bYbOa.css +1 -0
@@ -0,0 +1,2162 @@
1
+ /**
2
+ * Web Server - HTTP and WebSocket server for web interface
3
+ *
4
+ * Purpose:
5
+ * - Serve React frontend application
6
+ * - Handle HTTP API requests
7
+ * - Manage WebSocket connections for real-time updates
8
+ * - File upload and project management
9
+ */
10
+
11
+ import express from 'express';
12
+ import { createServer } from 'http';
13
+ import { WebSocketServer } from 'ws';
14
+ import path from 'path';
15
+ import { promises as fs } from 'fs';
16
+ import { fileURLToPath } from 'url';
17
+
18
+ import {
19
+ INTERFACE_TYPES,
20
+ ORCHESTRATOR_ACTIONS,
21
+ HTTP_STATUS,
22
+ AGENT_MODES
23
+ } from '../utilities/constants.js';
24
+
25
+ // Import file explorer module
26
+ import { initFileExplorerModule } from '../modules/fileExplorer/index.js';
27
+
28
+ const __filename = fileURLToPath(import.meta.url);
29
+ const __dirname = path.dirname(__filename);
30
+
31
+ class WebServer {
32
+ constructor(orchestrator, logger, config = {}) {
33
+ this.orchestrator = orchestrator;
34
+ this.logger = logger;
35
+ this.config = config;
36
+
37
+ this.port = config.port || 3000;
38
+ this.host = config.host || 'localhost';
39
+
40
+ // Express app
41
+ this.app = express();
42
+ this.server = createServer(this.app);
43
+
44
+ // WebSocket server with CORS support
45
+ this.wss = new WebSocketServer({
46
+ server: this.server,
47
+ // Allow all origins for WebSocket connections
48
+ verifyClient: (info) => {
49
+ // Log connection attempt for debugging
50
+ this.logger?.info('WebSocket connection attempt', {
51
+ origin: info.origin,
52
+ host: info.req.headers.host,
53
+ userAgent: info.req.headers['user-agent'],
54
+ url: info.req.url
55
+ });
56
+
57
+ // Allow all origins (you can restrict this later if needed)
58
+ return true;
59
+ }
60
+ });
61
+
62
+ // Active WebSocket connections
63
+ this.connections = new Map();
64
+
65
+ // Session management
66
+ this.sessions = new Map();
67
+
68
+ // API Key Manager reference (will be set by LoxiaSystem)
69
+ this.apiKeyManager = null;
70
+
71
+ this.isRunning = false;
72
+ }
73
+
74
+ /**
75
+ * Initialize web server
76
+ * @returns {Promise<void>}
77
+ */
78
+ async initialize() {
79
+ try {
80
+ this.setupMiddleware();
81
+ this.setupRoutes();
82
+ this.setupWebSocket();
83
+
84
+ await this.startServer();
85
+
86
+ this.logger.info('Web server initialized', {
87
+ port: this.port,
88
+ host: this.host,
89
+ url: `http://${this.host}:${this.port}`
90
+ });
91
+
92
+ } catch (error) {
93
+ this.logger.error('Web server initialization failed', {
94
+ error: error.message,
95
+ stack: error.stack
96
+ });
97
+ throw error;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Setup Express middleware
103
+ * @private
104
+ */
105
+ setupMiddleware() {
106
+ // CORS middleware
107
+ this.app.use((req, res, next) => {
108
+ res.header('Access-Control-Allow-Origin', '*');
109
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
110
+ res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
111
+
112
+ if (req.method === 'OPTIONS') {
113
+ res.sendStatus(HTTP_STATUS.OK);
114
+ } else {
115
+ next();
116
+ }
117
+ });
118
+
119
+ // JSON parsing
120
+ this.app.use(express.json({ limit: '10mb' }));
121
+ this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
122
+
123
+ // Initialize file explorer module
124
+ this.fileExplorerModule = initFileExplorerModule({
125
+ showHidden: false,
126
+ restrictedPaths: [] // Can be configured as needed
127
+ });
128
+
129
+ // Mount file explorer routes
130
+ this.app.use('/api/file-explorer', this.fileExplorerModule.router);
131
+
132
+ // Static files (React build)
133
+ const staticPath = path.join(__dirname, '../../web-ui/build');
134
+ this.app.use(express.static(staticPath));
135
+
136
+ // Request logging
137
+ this.app.use((req, res, next) => {
138
+ this.logger.debug(`${req.method} ${req.path}`, {
139
+ ip: req.ip,
140
+ userAgent: req.get('User-Agent')
141
+ });
142
+ next();
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Setup API routes
148
+ * @private
149
+ */
150
+ setupRoutes() {
151
+ // Health check
152
+ this.app.get('/api/health', async (req, res) => {
153
+ try {
154
+ const packageJsonPath = path.join(__dirname, '../../package.json');
155
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
156
+
157
+ res.json({
158
+ status: 'healthy',
159
+ version: packageJson.version || '1.0.0',
160
+ timestamp: new Date().toISOString()
161
+ });
162
+ } catch (error) {
163
+ res.json({
164
+ status: 'healthy',
165
+ version: '1.0.0',
166
+ timestamp: new Date().toISOString()
167
+ });
168
+ }
169
+ });
170
+
171
+ // Session creation
172
+ this.app.post('/api/sessions', async (req, res) => {
173
+ try {
174
+ const sessionId = `web-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
175
+ const projectDir = req.body.projectDir || process.cwd();
176
+
177
+ const session = {
178
+ id: sessionId,
179
+ projectDir,
180
+ createdAt: new Date().toISOString(),
181
+ lastActivity: new Date().toISOString()
182
+ };
183
+
184
+ this.sessions.set(sessionId, session);
185
+
186
+ res.json({
187
+ success: true,
188
+ session
189
+ });
190
+
191
+ } catch (error) {
192
+ res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
193
+ success: false,
194
+ error: error.message
195
+ });
196
+ }
197
+ });
198
+
199
+ // Orchestrator API proxy
200
+ this.app.post('/api/orchestrator', async (req, res) => {
201
+ try {
202
+ const request = {
203
+ interface: INTERFACE_TYPES.WEB,
204
+ sessionId: req.body.sessionId,
205
+ action: req.body.action,
206
+ payload: req.body.payload,
207
+ projectDir: req.body.projectDir || process.cwd()
208
+ };
209
+
210
+ const response = await this.orchestrator.processRequest(request);
211
+
212
+ // Broadcast updates via WebSocket
213
+ this.broadcastToSession(request.sessionId, {
214
+ type: 'orchestrator_response',
215
+ action: request.action,
216
+ response
217
+ });
218
+
219
+ res.json(response);
220
+
221
+ } catch (error) {
222
+ this.logger.error('Orchestrator API error', {
223
+ error: error.message,
224
+ body: req.body
225
+ });
226
+
227
+ res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
228
+ success: false,
229
+ error: error.message
230
+ });
231
+ }
232
+ });
233
+
234
+ // File operations
235
+ this.app.get('/api/files', async (req, res) => {
236
+ try {
237
+ const { path: filePath, projectDir } = req.query;
238
+ const fullPath = path.resolve(projectDir || process.cwd(), filePath || '.');
239
+
240
+ const stats = await fs.stat(fullPath);
241
+
242
+ if (stats.isDirectory()) {
243
+ const entries = await fs.readdir(fullPath, { withFileTypes: true });
244
+ const files = entries.map(entry => ({
245
+ name: entry.name,
246
+ type: entry.isDirectory() ? 'directory' : 'file',
247
+ path: path.join(filePath || '.', entry.name)
248
+ }));
249
+
250
+ res.json({ success: true, files });
251
+ } else {
252
+ const content = await fs.readFile(fullPath, 'utf8');
253
+ res.json({ success: true, content, type: 'file' });
254
+ }
255
+
256
+ } catch (error) {
257
+ res.status(HTTP_STATUS.NOT_FOUND).json({
258
+ success: false,
259
+ error: error.message
260
+ });
261
+ }
262
+ });
263
+
264
+ // File upload
265
+ this.app.post('/api/files/upload', async (req, res) => {
266
+ try {
267
+ const { fileName, content, projectDir } = req.body;
268
+ const targetDir = projectDir || process.cwd();
269
+ const fullPath = path.resolve(targetDir, fileName);
270
+
271
+ // Ensure the directory exists
272
+ await fs.mkdir(targetDir, { recursive: true });
273
+
274
+ // Write the file
275
+ await fs.writeFile(fullPath, content, 'utf8');
276
+
277
+ res.json({
278
+ success: true,
279
+ message: 'File uploaded successfully',
280
+ path: fullPath
281
+ });
282
+
283
+ } catch (error) {
284
+ res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
285
+ success: false,
286
+ error: error.message
287
+ });
288
+ }
289
+ });
290
+
291
+ // Enhanced folder explorer endpoint
292
+ this.app.get('/api/explorer', async (req, res) => {
293
+ try {
294
+ const { path: requestedPath, showHidden = false } = req.query;
295
+ const basePath = requestedPath || process.cwd();
296
+ const fullPath = path.resolve(basePath);
297
+
298
+ // Security: Basic path traversal protection
299
+ const normalizedPath = path.normalize(fullPath);
300
+
301
+ const stats = await fs.stat(normalizedPath);
302
+
303
+ if (!stats.isDirectory()) {
304
+ return res.status(HTTP_STATUS.BAD_REQUEST).json({
305
+ success: false,
306
+ error: 'Path is not a directory'
307
+ });
308
+ }
309
+
310
+ const entries = await fs.readdir(normalizedPath, { withFileTypes: true });
311
+
312
+ // Process entries
313
+ const items = await Promise.all(
314
+ entries
315
+ .filter(entry => showHidden === 'true' || !entry.name.startsWith('.'))
316
+ .map(async (entry) => {
317
+ const itemPath = path.join(normalizedPath, entry.name);
318
+ const itemStats = await fs.stat(itemPath).catch(() => null);
319
+
320
+ return {
321
+ name: entry.name,
322
+ type: entry.isDirectory() ? 'directory' : 'file',
323
+ path: itemPath,
324
+ relativePath: path.relative(process.cwd(), itemPath),
325
+ size: itemStats?.size || 0,
326
+ modified: itemStats?.mtime || null,
327
+ isHidden: entry.name.startsWith('.'),
328
+ permissions: {
329
+ readable: true, // Could check with fs.access
330
+ writable: true // Could check with fs.access
331
+ }
332
+ };
333
+ })
334
+ );
335
+
336
+ // Sort: directories first, then files, both alphabetically
337
+ items.sort((a, b) => {
338
+ if (a.type !== b.type) {
339
+ return a.type === 'directory' ? -1 : 1;
340
+ }
341
+ return a.name.localeCompare(b.name);
342
+ });
343
+
344
+ // Get parent directory info
345
+ const parentPath = path.dirname(normalizedPath);
346
+ const hasParent = parentPath !== normalizedPath;
347
+
348
+ res.json({
349
+ success: true,
350
+ currentPath: normalizedPath,
351
+ currentRelativePath: path.relative(process.cwd(), normalizedPath),
352
+ parentPath: hasParent ? parentPath : null,
353
+ items,
354
+ totalItems: items.length,
355
+ directories: items.filter(item => item.type === 'directory').length,
356
+ files: items.filter(item => item.type === 'file').length
357
+ });
358
+
359
+ } catch (error) {
360
+ res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
361
+ success: false,
362
+ error: error.message,
363
+ code: error.code
364
+ });
365
+ }
366
+ });
367
+
368
+ // Create directory endpoint
369
+ this.app.post('/api/explorer/mkdir', async (req, res) => {
370
+ try {
371
+ const { path: dirPath, name } = req.body;
372
+ const fullPath = path.resolve(dirPath, name);
373
+
374
+ await fs.mkdir(fullPath, { recursive: false });
375
+
376
+ res.json({
377
+ success: true,
378
+ path: fullPath,
379
+ relativePath: path.relative(process.cwd(), fullPath)
380
+ });
381
+
382
+ } catch (error) {
383
+ res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
384
+ success: false,
385
+ error: error.message,
386
+ code: error.code
387
+ });
388
+ }
389
+ });
390
+
391
+ // Get directory info endpoint
392
+ this.app.get('/api/explorer/info', async (req, res) => {
393
+ try {
394
+ const { path: requestedPath } = req.query;
395
+ const fullPath = path.resolve(requestedPath);
396
+
397
+ const stats = await fs.stat(fullPath);
398
+
399
+ res.json({
400
+ success: true,
401
+ path: fullPath,
402
+ relativePath: path.relative(process.cwd(), fullPath),
403
+ isDirectory: stats.isDirectory(),
404
+ isFile: stats.isFile(),
405
+ size: stats.size,
406
+ created: stats.birthtime,
407
+ modified: stats.mtime,
408
+ accessed: stats.atime
409
+ });
410
+
411
+ } catch (error) {
412
+ res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
413
+ success: false,
414
+ error: error.message,
415
+ code: error.code
416
+ });
417
+ }
418
+ });
419
+
420
+ // LLM Chat endpoint - proxy to Loxia Azure backend
421
+ this.app.post('/api/llm/chat', async (req, res) => {
422
+ try {
423
+ const { sessionId, model, platformProvided } = req.body;
424
+
425
+ if (!sessionId) {
426
+ return res.status(400).json({
427
+ error: 'Session ID is required'
428
+ });
429
+ }
430
+
431
+ // Get API keys from session-based storage
432
+ let apiKey = null;
433
+ let vendorApiKey = null;
434
+
435
+ if (this.apiKeyManager) {
436
+ const keys = this.apiKeyManager.getKeysForRequest(sessionId, {
437
+ platformProvided: platformProvided || false,
438
+ vendor: this._getVendorFromModel(model)
439
+ });
440
+
441
+ apiKey = keys.loxiaApiKey;
442
+ vendorApiKey = keys.vendorApiKey;
443
+ }
444
+
445
+ // Fallback to config/environment if no session keys
446
+ if (!apiKey) {
447
+ apiKey = this.config.loxiaApiKey || process.env.LOXIA_API_KEY;
448
+ }
449
+
450
+ // Also try API key from request body for backward compatibility
451
+ if (!apiKey && req.body.apiKey) {
452
+ apiKey = req.body.apiKey;
453
+ }
454
+
455
+ if (!apiKey) {
456
+ this.logger.warn('No API key available for chat request', {
457
+ sessionId,
458
+ model,
459
+ platformProvided,
460
+ hasSessionManager: !!this.apiKeyManager
461
+ });
462
+
463
+ return res.status(401).json({
464
+ error: 'No API key configured. Please configure your Loxia API key in Settings.'
465
+ });
466
+ }
467
+
468
+ // Make request to Loxia Azure backend
469
+ const azureBackendUrl = 'https://autopilot-api.azurewebsites.net/llm/chat';
470
+
471
+ // Prepare request payload with API keys
472
+ const requestPayload = {
473
+ ...req.body,
474
+ apiKey, // Loxia platform API key
475
+ ...(vendorApiKey && { vendorApiKey }) // Include vendor key if available
476
+ };
477
+
478
+ const fetchOptions = {
479
+ method: 'POST',
480
+ headers: {
481
+ 'Content-Type': 'application/json',
482
+ 'Authorization': `Bearer ${apiKey}`
483
+ },
484
+ body: JSON.stringify(requestPayload)
485
+ };
486
+
487
+ this.logger.info('Proxying chat request to Loxia Azure backend', {
488
+ url: azureBackendUrl,
489
+ model: req.body.model,
490
+ hasApiKey: !!apiKey
491
+ });
492
+
493
+ const response = await fetch(azureBackendUrl, fetchOptions);
494
+
495
+ if (response.ok) {
496
+ const data = await response.json();
497
+ this.logger.info('Successfully received response from Azure backend', {
498
+ model: data.model,
499
+ contentLength: data.content?.length || 0
500
+ });
501
+
502
+ res.json(data);
503
+ } else {
504
+ const errorText = await response.text();
505
+ this.logger.warn('Azure backend chat request failed', {
506
+ status: response.status,
507
+ statusText: response.statusText,
508
+ error: errorText
509
+ });
510
+
511
+ res.status(response.status).json({
512
+ error: `Azure backend error: ${response.status} ${response.statusText}`,
513
+ details: errorText
514
+ });
515
+ }
516
+
517
+ } catch (error) {
518
+ this.logger.error('Failed to proxy chat request to Azure backend', {
519
+ error: error.message,
520
+ stack: error.stack
521
+ });
522
+
523
+ res.status(500).json({
524
+ error: 'Failed to connect to AI service',
525
+ details: error.message
526
+ });
527
+ }
528
+ });
529
+
530
+ // LLM Models endpoint - proxy to Loxia Azure backend
531
+ this.app.get('/api/llm/models', async (req, res) => {
532
+ try {
533
+ // Get API key from authorization header if provided
534
+ const authHeader = req.headers.authorization;
535
+ let apiKey = null;
536
+
537
+ if (authHeader && authHeader.startsWith('Bearer ')) {
538
+ apiKey = authHeader.substring(7);
539
+ }
540
+
541
+ // If no API key, try to get from config or environment
542
+ if (!apiKey) {
543
+ apiKey = this.config.loxiaApiKey || process.env.LOXIA_API_KEY;
544
+ }
545
+
546
+ // Make request to Loxia Azure backend
547
+ const azureBackendUrl = 'https://autopilot-api.azurewebsites.net/llm/models';
548
+
549
+ const fetchOptions = {
550
+ method: 'GET',
551
+ headers: {
552
+ 'Content-Type': 'application/json',
553
+ ...(apiKey && { 'Authorization': `Bearer ${apiKey}` })
554
+ }
555
+ };
556
+
557
+ this.logger.info('Fetching models from Loxia Azure backend', {
558
+ url: azureBackendUrl,
559
+ hasApiKey: !!apiKey
560
+ });
561
+
562
+ const response = await fetch(azureBackendUrl, fetchOptions);
563
+
564
+ if (response.ok) {
565
+ const data = await response.json();
566
+ this.logger.info('Successfully fetched models from Azure backend', {
567
+ modelCount: data.models?.length || 0
568
+ });
569
+
570
+ res.json(data);
571
+ } else {
572
+ // If authentication failed or other error, return fallback models
573
+ const errorText = await response.text();
574
+ this.logger.warn('Azure backend request failed, using fallback models', {
575
+ status: response.status,
576
+ statusText: response.statusText,
577
+ error: errorText
578
+ });
579
+
580
+ res.json({
581
+ models: this.getDefaultModels(),
582
+ total: this.getDefaultModels().length,
583
+ fallback: true,
584
+ reason: `Azure backend error: ${response.status} ${response.statusText}`
585
+ });
586
+ }
587
+
588
+ } catch (error) {
589
+ this.logger.error('Failed to fetch models from Azure backend', {
590
+ error: error.message,
591
+ stack: error.stack
592
+ });
593
+
594
+ // Return fallback models on network or other errors
595
+ res.json({
596
+ models: this.getDefaultModels(),
597
+ total: this.getDefaultModels().length,
598
+ fallback: true,
599
+ error: error.message
600
+ });
601
+ }
602
+ });
603
+
604
+ // Tools information endpoint - get available tools from registry
605
+ this.app.get('/api/tools', async (req, res) => {
606
+ try {
607
+ // Get tools registry (passed from LoxiaSystem)
608
+ const toolsRegistry = this.toolsRegistry;
609
+
610
+ if (!toolsRegistry) {
611
+ return res.status(500).json({
612
+ error: 'Tools registry not available'
613
+ });
614
+ }
615
+
616
+ // Get available tools for UI
617
+ const tools = toolsRegistry.getAvailableToolsForUI();
618
+
619
+ this.logger.info('Serving tools information', {
620
+ toolCount: tools.length,
621
+ tools: tools.map(t => ({ id: t.id, name: t.name, category: t.category }))
622
+ });
623
+
624
+ res.json({
625
+ success: true,
626
+ tools,
627
+ total: tools.length
628
+ });
629
+
630
+ } catch (error) {
631
+ this.logger.error('Failed to get tools information', {
632
+ error: error.message,
633
+ stack: error.stack
634
+ });
635
+
636
+ res.status(500).json({
637
+ error: 'Failed to retrieve tools information',
638
+ message: error.message
639
+ });
640
+ }
641
+ });
642
+
643
+ // API Key Management Endpoints
644
+
645
+ // Set API keys for current session
646
+ this.app.post('/api/keys', async (req, res) => {
647
+ try {
648
+ const { sessionId, loxiaApiKey, vendorKeys } = req.body;
649
+
650
+ if (!sessionId) {
651
+ return res.status(400).json({
652
+ success: false,
653
+ error: 'Session ID is required'
654
+ });
655
+ }
656
+
657
+ if (!this.apiKeyManager) {
658
+ return res.status(500).json({
659
+ success: false,
660
+ error: 'API key manager not available'
661
+ });
662
+ }
663
+
664
+ // Set API keys for the session
665
+ this.apiKeyManager.setSessionKeys(sessionId, {
666
+ loxiaApiKey,
667
+ vendorKeys: vendorKeys || {}
668
+ });
669
+
670
+ this.logger.info('API keys updated for session', {
671
+ sessionId,
672
+ hasLoxiaKey: !!loxiaApiKey,
673
+ vendorKeys: Object.keys(vendorKeys || {})
674
+ });
675
+
676
+ // Refresh models with the new API key context
677
+ if (this.orchestrator && this.orchestrator.modelsService && loxiaApiKey) {
678
+ try {
679
+ await this.orchestrator.modelsService.refresh({ sessionId, apiKey: loxiaApiKey });
680
+ this.logger.info('Models refreshed with new API key', { sessionId });
681
+ } catch (error) {
682
+ this.logger.warn('Failed to refresh models after API key update', {
683
+ error: error.message,
684
+ sessionId
685
+ });
686
+ }
687
+ }
688
+
689
+ res.json({
690
+ success: true,
691
+ message: 'API keys updated successfully',
692
+ sessionId,
693
+ hasLoxiaKey: !!loxiaApiKey,
694
+ vendorKeys: Object.keys(vendorKeys || {})
695
+ });
696
+
697
+ } catch (error) {
698
+ this.logger.error('Failed to set API keys', {
699
+ error: error.message,
700
+ sessionId: req.body.sessionId
701
+ });
702
+
703
+ res.status(500).json({
704
+ success: false,
705
+ error: error.message
706
+ });
707
+ }
708
+ });
709
+
710
+ // Get API keys for current session (returns only presence, not values)
711
+ this.app.get('/api/keys/:sessionId', async (req, res) => {
712
+ try {
713
+ const { sessionId } = req.params;
714
+
715
+ if (!this.apiKeyManager) {
716
+ return res.status(500).json({
717
+ success: false,
718
+ error: 'API key manager not available'
719
+ });
720
+ }
721
+
722
+ const keys = this.apiKeyManager.getSessionKeys(sessionId);
723
+
724
+ // Return key presence only, not actual values
725
+ res.json({
726
+ success: true,
727
+ sessionId,
728
+ hasLoxiaKey: !!keys.loxiaApiKey,
729
+ vendorKeys: Object.keys(keys.vendorKeys || {}),
730
+ updatedAt: keys.updatedAt
731
+ });
732
+
733
+ } catch (error) {
734
+ this.logger.error('Failed to get API key status', {
735
+ error: error.message,
736
+ sessionId: req.params.sessionId
737
+ });
738
+
739
+ res.status(500).json({
740
+ success: false,
741
+ error: error.message
742
+ });
743
+ }
744
+ });
745
+
746
+ // Remove API keys for current session
747
+ this.app.delete('/api/keys/:sessionId', async (req, res) => {
748
+ try {
749
+ const { sessionId } = req.params;
750
+
751
+ if (!this.apiKeyManager) {
752
+ return res.status(500).json({
753
+ success: false,
754
+ error: 'API key manager not available'
755
+ });
756
+ }
757
+
758
+ const removed = this.apiKeyManager.removeSessionKeys(sessionId);
759
+
760
+ res.json({
761
+ success: true,
762
+ removed,
763
+ message: removed ? 'API keys removed successfully' : 'No API keys found for session'
764
+ });
765
+
766
+ } catch (error) {
767
+ this.logger.error('Failed to remove API keys', {
768
+ error: error.message,
769
+ sessionId: req.params.sessionId
770
+ });
771
+
772
+ res.status(500).json({
773
+ success: false,
774
+ error: error.message
775
+ });
776
+ }
777
+ });
778
+
779
+ // Get active sessions with API keys (admin endpoint)
780
+ this.app.get('/api/keys', async (req, res) => {
781
+ try {
782
+ if (!this.apiKeyManager) {
783
+ return res.status(500).json({
784
+ success: false,
785
+ error: 'API key manager not available'
786
+ });
787
+ }
788
+
789
+ const activeSessions = this.apiKeyManager.getActiveSessions();
790
+
791
+ res.json({
792
+ success: true,
793
+ sessions: activeSessions,
794
+ total: activeSessions.length
795
+ });
796
+
797
+ } catch (error) {
798
+ this.logger.error('Failed to get active sessions', {
799
+ error: error.message
800
+ });
801
+
802
+ res.status(500).json({
803
+ success: false,
804
+ error: error.message
805
+ });
806
+ }
807
+ });
808
+
809
+ // File Attachments Management Endpoints
810
+
811
+ // Upload file attachment for agent
812
+ this.app.post('/api/agents/:agentId/attachments/upload', async (req, res) => {
813
+ try {
814
+ const { agentId } = req.params;
815
+ const { filePath, mode, fileName } = req.body;
816
+
817
+ if (!this.orchestrator?.fileAttachmentService) {
818
+ return res.status(500).json({
819
+ success: false,
820
+ error: 'File attachment service not available'
821
+ });
822
+ }
823
+
824
+ const result = await this.orchestrator.fileAttachmentService.uploadFile({
825
+ agentId,
826
+ filePath,
827
+ mode: mode || 'content',
828
+ fileName
829
+ });
830
+
831
+ this.logger.info('File attachment uploaded', {
832
+ agentId,
833
+ fileId: result.fileId,
834
+ fileName: result.fileName
835
+ });
836
+
837
+ res.json({
838
+ success: true,
839
+ attachment: result
840
+ });
841
+
842
+ } catch (error) {
843
+ this.logger.error('Failed to upload file attachment', {
844
+ agentId: req.params.agentId,
845
+ error: error.message
846
+ });
847
+
848
+ res.status(500).json({
849
+ success: false,
850
+ error: error.message
851
+ });
852
+ }
853
+ });
854
+
855
+ // Get all attachments for an agent
856
+ this.app.get('/api/agents/:agentId/attachments', async (req, res) => {
857
+ try {
858
+ const { agentId } = req.params;
859
+ const { mode, active } = req.query;
860
+
861
+ if (!this.orchestrator?.fileAttachmentService) {
862
+ return res.status(500).json({
863
+ success: false,
864
+ error: 'File attachment service not available'
865
+ });
866
+ }
867
+
868
+ const filters = {};
869
+ if (mode) filters.mode = mode;
870
+ if (active !== undefined) filters.active = active === 'true';
871
+
872
+ const attachments = await this.orchestrator.fileAttachmentService.getAttachments(agentId, filters);
873
+
874
+ res.json({
875
+ success: true,
876
+ attachments,
877
+ total: attachments.length
878
+ });
879
+
880
+ } catch (error) {
881
+ this.logger.error('Failed to get attachments', {
882
+ agentId: req.params.agentId,
883
+ error: error.message
884
+ });
885
+
886
+ res.status(500).json({
887
+ success: false,
888
+ error: error.message
889
+ });
890
+ }
891
+ });
892
+
893
+ // Get single attachment by ID
894
+ this.app.get('/api/attachments/:fileId', async (req, res) => {
895
+ try {
896
+ const { fileId } = req.params;
897
+
898
+ if (!this.orchestrator?.fileAttachmentService) {
899
+ return res.status(500).json({
900
+ success: false,
901
+ error: 'File attachment service not available'
902
+ });
903
+ }
904
+
905
+ const attachment = await this.orchestrator.fileAttachmentService.getAttachment(fileId);
906
+
907
+ if (!attachment) {
908
+ return res.status(404).json({
909
+ success: false,
910
+ error: 'Attachment not found'
911
+ });
912
+ }
913
+
914
+ res.json({
915
+ success: true,
916
+ attachment
917
+ });
918
+
919
+ } catch (error) {
920
+ this.logger.error('Failed to get attachment', {
921
+ fileId: req.params.fileId,
922
+ error: error.message
923
+ });
924
+
925
+ res.status(500).json({
926
+ success: false,
927
+ error: error.message
928
+ });
929
+ }
930
+ });
931
+
932
+ // Toggle attachment active status
933
+ this.app.patch('/api/attachments/:fileId/toggle', async (req, res) => {
934
+ try {
935
+ const { fileId } = req.params;
936
+
937
+ if (!this.orchestrator?.fileAttachmentService) {
938
+ return res.status(500).json({
939
+ success: false,
940
+ error: 'File attachment service not available'
941
+ });
942
+ }
943
+
944
+ const result = await this.orchestrator.fileAttachmentService.toggleActive(fileId);
945
+
946
+ this.logger.info('Attachment active status toggled', {
947
+ fileId,
948
+ active: result.active
949
+ });
950
+
951
+ res.json({
952
+ success: true,
953
+ attachment: result
954
+ });
955
+
956
+ } catch (error) {
957
+ this.logger.error('Failed to toggle attachment', {
958
+ fileId: req.params.fileId,
959
+ error: error.message
960
+ });
961
+
962
+ res.status(500).json({
963
+ success: false,
964
+ error: error.message
965
+ });
966
+ }
967
+ });
968
+
969
+ // Update attachment metadata
970
+ this.app.patch('/api/attachments/:fileId', async (req, res) => {
971
+ try {
972
+ const { fileId } = req.params;
973
+ const { mode, active } = req.body;
974
+
975
+ if (!this.orchestrator?.fileAttachmentService) {
976
+ return res.status(500).json({
977
+ success: false,
978
+ error: 'File attachment service not available'
979
+ });
980
+ }
981
+
982
+ const updates = {};
983
+ if (mode !== undefined) updates.mode = mode;
984
+ if (active !== undefined) updates.active = active;
985
+
986
+ const result = await this.orchestrator.fileAttachmentService.updateAttachment(fileId, updates);
987
+
988
+ this.logger.info('Attachment updated', {
989
+ fileId,
990
+ updates
991
+ });
992
+
993
+ res.json({
994
+ success: true,
995
+ attachment: result
996
+ });
997
+
998
+ } catch (error) {
999
+ this.logger.error('Failed to update attachment', {
1000
+ fileId: req.params.fileId,
1001
+ error: error.message
1002
+ });
1003
+
1004
+ res.status(500).json({
1005
+ success: false,
1006
+ error: error.message
1007
+ });
1008
+ }
1009
+ });
1010
+
1011
+ // Delete attachment
1012
+ this.app.delete('/api/attachments/:fileId', async (req, res) => {
1013
+ try {
1014
+ const { fileId } = req.params;
1015
+ const { agentId } = req.query;
1016
+
1017
+ if (!agentId) {
1018
+ return res.status(400).json({
1019
+ success: false,
1020
+ error: 'agentId query parameter is required'
1021
+ });
1022
+ }
1023
+
1024
+ if (!this.orchestrator?.fileAttachmentService) {
1025
+ return res.status(500).json({
1026
+ success: false,
1027
+ error: 'File attachment service not available'
1028
+ });
1029
+ }
1030
+
1031
+ const result = await this.orchestrator.fileAttachmentService.deleteAttachment(fileId, agentId);
1032
+
1033
+ this.logger.info('Attachment deleted', {
1034
+ fileId,
1035
+ agentId,
1036
+ physicallyDeleted: result.physicallyDeleted
1037
+ });
1038
+
1039
+ res.json({
1040
+ success: true,
1041
+ message: result.physicallyDeleted ? 'Attachment deleted' : 'Reference removed',
1042
+ physicallyDeleted: result.physicallyDeleted
1043
+ });
1044
+
1045
+ } catch (error) {
1046
+ this.logger.error('Failed to delete attachment', {
1047
+ fileId: req.params.fileId,
1048
+ error: error.message
1049
+ });
1050
+
1051
+ res.status(500).json({
1052
+ success: false,
1053
+ error: error.message
1054
+ });
1055
+ }
1056
+ });
1057
+
1058
+ // Import attachment from another agent
1059
+ this.app.post('/api/attachments/:fileId/import', async (req, res) => {
1060
+ try {
1061
+ const { fileId } = req.params;
1062
+ const { targetAgentId } = req.body;
1063
+
1064
+ if (!targetAgentId) {
1065
+ return res.status(400).json({
1066
+ success: false,
1067
+ error: 'targetAgentId is required'
1068
+ });
1069
+ }
1070
+
1071
+ if (!this.orchestrator?.fileAttachmentService) {
1072
+ return res.status(500).json({
1073
+ success: false,
1074
+ error: 'File attachment service not available'
1075
+ });
1076
+ }
1077
+
1078
+ const result = await this.orchestrator.fileAttachmentService.importFromAgent(fileId, targetAgentId);
1079
+
1080
+ this.logger.info('Attachment imported', {
1081
+ fileId,
1082
+ targetAgentId
1083
+ });
1084
+
1085
+ res.json({
1086
+ success: true,
1087
+ attachment: result
1088
+ });
1089
+
1090
+ } catch (error) {
1091
+ this.logger.error('Failed to import attachment', {
1092
+ fileId: req.params.fileId,
1093
+ error: error.message
1094
+ });
1095
+
1096
+ res.status(500).json({
1097
+ success: false,
1098
+ error: error.message
1099
+ });
1100
+ }
1101
+ });
1102
+
1103
+ // Get attachment preview
1104
+ this.app.get('/api/attachments/:fileId/preview', async (req, res) => {
1105
+ try {
1106
+ const { fileId } = req.params;
1107
+
1108
+ if (!this.orchestrator?.fileAttachmentService) {
1109
+ return res.status(500).json({
1110
+ success: false,
1111
+ error: 'File attachment service not available'
1112
+ });
1113
+ }
1114
+
1115
+ const preview = await this.orchestrator.fileAttachmentService.getAttachmentPreview(fileId);
1116
+
1117
+ if (!preview) {
1118
+ return res.status(404).json({
1119
+ success: false,
1120
+ error: 'Attachment not found'
1121
+ });
1122
+ }
1123
+
1124
+ res.json({
1125
+ success: true,
1126
+ preview
1127
+ });
1128
+
1129
+ } catch (error) {
1130
+ this.logger.error('Failed to get attachment preview', {
1131
+ fileId: req.params.fileId,
1132
+ error: error.message
1133
+ });
1134
+
1135
+ res.status(500).json({
1136
+ success: false,
1137
+ error: error.message
1138
+ });
1139
+ }
1140
+ });
1141
+
1142
+ // Image serving endpoint for generated images
1143
+ this.app.get('/api/images/:sessionId/:filename', async (req, res) => {
1144
+ try {
1145
+ const { sessionId, filename } = req.params;
1146
+
1147
+ // Security validation: Check if sessionId exists
1148
+ const session = this.sessions.get(sessionId);
1149
+ if (!session) {
1150
+ return res.status(HTTP_STATUS.FORBIDDEN).json({
1151
+ success: false,
1152
+ error: 'Invalid session'
1153
+ });
1154
+ }
1155
+
1156
+ // Security validation: Check filename for path traversal attempts
1157
+ const normalizedFilename = path.basename(filename);
1158
+ if (normalizedFilename !== filename || filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
1159
+ return res.status(HTTP_STATUS.BAD_REQUEST).json({
1160
+ success: false,
1161
+ error: 'Invalid filename'
1162
+ });
1163
+ }
1164
+
1165
+ // Try to locate the image in multiple possible locations
1166
+ let imagePath = null;
1167
+ const searchPaths = [
1168
+ // 1. Session's project directory images folder
1169
+ path.join(session.projectDir || process.cwd(), 'images', normalizedFilename),
1170
+ // 2. Temp directory for this session
1171
+ path.join('/tmp/loxia-images', sessionId, normalizedFilename),
1172
+ // 3. General temp images directory
1173
+ path.join('/tmp/loxia-images', normalizedFilename)
1174
+ ];
1175
+
1176
+ // 4. Search in agent working directories for this session
1177
+ // Get all agents and check their workingDirectory
1178
+ if (this.orchestrator?.agentPool) {
1179
+ try {
1180
+ const agents = await this.orchestrator.agentPool.getAllAgents();
1181
+ for (const agent of agents) {
1182
+ if (agent.directoryAccess?.workingDirectory) {
1183
+ searchPaths.push(
1184
+ path.join(agent.directoryAccess.workingDirectory, 'images', normalizedFilename)
1185
+ );
1186
+ }
1187
+ }
1188
+ } catch (error) {
1189
+ this.logger.warn('Failed to get agent working directories for image search', {
1190
+ error: error.message
1191
+ });
1192
+ }
1193
+ }
1194
+
1195
+ // Find the first existing file
1196
+ for (const searchPath of searchPaths) {
1197
+ try {
1198
+ const stats = await fs.stat(searchPath);
1199
+ if (stats.isFile()) {
1200
+ imagePath = searchPath;
1201
+ this.logger.info('Image found', {
1202
+ filename: normalizedFilename,
1203
+ path: imagePath,
1204
+ sessionId
1205
+ });
1206
+ break;
1207
+ }
1208
+ } catch (error) {
1209
+ // File doesn't exist at this path, try next one
1210
+ continue;
1211
+ }
1212
+ }
1213
+
1214
+ if (!imagePath) {
1215
+ this.logger.warn('Image not found in any search path', {
1216
+ filename: normalizedFilename,
1217
+ sessionId,
1218
+ searchPaths
1219
+ });
1220
+
1221
+ return res.status(HTTP_STATUS.NOT_FOUND).json({
1222
+ success: false,
1223
+ error: 'Image not found'
1224
+ });
1225
+ }
1226
+
1227
+ // Serve the image file
1228
+ res.sendFile(imagePath, (err) => {
1229
+ if (err) {
1230
+ this.logger.error('Failed to send image file', {
1231
+ error: err.message,
1232
+ imagePath,
1233
+ filename: normalizedFilename
1234
+ });
1235
+
1236
+ if (!res.headersSent) {
1237
+ res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
1238
+ success: false,
1239
+ error: 'Failed to serve image'
1240
+ });
1241
+ }
1242
+ } else {
1243
+ this.logger.info('Image served successfully', {
1244
+ filename: normalizedFilename,
1245
+ path: imagePath,
1246
+ sessionId
1247
+ });
1248
+ }
1249
+ });
1250
+
1251
+ } catch (error) {
1252
+ this.logger.error('Image serving error', {
1253
+ error: error.message,
1254
+ sessionId: req.params.sessionId,
1255
+ filename: req.params.filename
1256
+ });
1257
+
1258
+ res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
1259
+ success: false,
1260
+ error: error.message
1261
+ });
1262
+ }
1263
+ });
1264
+
1265
+ // Agent mode control endpoints
1266
+ this.app.post('/api/agents/:agentId/mode', async (req, res) => {
1267
+ try {
1268
+ const { agentId } = req.params;
1269
+ const { mode, lockMode = false, sessionId: bodySessionId } = req.body;
1270
+
1271
+ // Validate mode
1272
+ if (!Object.values(AGENT_MODES).includes(mode)) {
1273
+ return res.status(400).json({
1274
+ success: false,
1275
+ error: `Invalid mode. Must be one of: ${Object.values(AGENT_MODES).join(', ')}`
1276
+ });
1277
+ }
1278
+
1279
+ // CRITICAL FIX: Use session ID from body first, then req.sessionID
1280
+ const sessionId = bodySessionId || req.sessionID;
1281
+
1282
+ if (!sessionId) {
1283
+ this.logger.warn('Agent mode update requested without session ID', {
1284
+ agentId,
1285
+ hasBodySessionId: !!bodySessionId,
1286
+ hasReqSessionId: !!req.sessionID
1287
+ });
1288
+ }
1289
+
1290
+ // Update agent mode
1291
+ const request = {
1292
+ interface: INTERFACE_TYPES.WEB,
1293
+ sessionId: sessionId,
1294
+ action: 'update_agent',
1295
+ payload: {
1296
+ agentId,
1297
+ updates: {
1298
+ mode: mode // lockMode is no longer used, only CHAT and AGENT modes
1299
+ }
1300
+ }
1301
+ };
1302
+
1303
+ const response = await this.orchestrator.processRequest(request);
1304
+
1305
+ if (response.success) {
1306
+ // Extract agent from response.data (orchestrator wraps result in data property)
1307
+ const updatedAgent = response.data;
1308
+
1309
+ this.logger.info(`Agent mode updated: ${agentId}`, {
1310
+ newMode: mode,
1311
+ lockMode,
1312
+ finalMode: updatedAgent?.mode
1313
+ });
1314
+
1315
+ // Broadcast mode change via WebSocket
1316
+ this.broadcastToSession(request.sessionId, {
1317
+ type: 'agent_mode_changed',
1318
+ agentId,
1319
+ mode: updatedAgent?.mode,
1320
+ modeState: updatedAgent?.modeState
1321
+ });
1322
+
1323
+ res.json({
1324
+ success: true,
1325
+ agent: updatedAgent,
1326
+ message: `Agent mode switched to ${updatedAgent?.mode}`
1327
+ });
1328
+ } else {
1329
+ res.status(400).json({
1330
+ success: false,
1331
+ error: response.error || 'Failed to update agent mode'
1332
+ });
1333
+ }
1334
+
1335
+ } catch (error) {
1336
+ this.logger.error('Failed to update agent mode', {
1337
+ agentId: req.params.agentId,
1338
+ error: error.message
1339
+ });
1340
+
1341
+ res.status(500).json({
1342
+ success: false,
1343
+ error: error.message
1344
+ });
1345
+ }
1346
+ });
1347
+
1348
+ // Stop autonomous execution
1349
+ this.app.post('/api/agents/:agentId/stop', async (req, res) => {
1350
+ try {
1351
+ const { agentId } = req.params;
1352
+
1353
+ // Get message processor from orchestrator
1354
+ const messageProcessor = this.orchestrator.messageProcessor;
1355
+ if (!messageProcessor) {
1356
+ return res.status(500).json({
1357
+ success: false,
1358
+ error: 'Message processor not available'
1359
+ });
1360
+ }
1361
+
1362
+ const result = await messageProcessor.stopAutonomousExecution(agentId);
1363
+
1364
+ this.logger.info(`Autonomous execution stop requested: ${agentId}`);
1365
+
1366
+ // Broadcast stop event via WebSocket with updated agent state
1367
+ // TODO: Get session ID from request body or agent context
1368
+ const broadcastSessionId = req.sessionID || result.agent?.sessionId;
1369
+ if (broadcastSessionId) {
1370
+ this.broadcastToSession(broadcastSessionId, {
1371
+ type: 'agent_mode_changed',
1372
+ agentId,
1373
+ mode: result.agent?.mode,
1374
+ modeState: result.agent?.modeState
1375
+ });
1376
+ }
1377
+
1378
+ res.json(result);
1379
+
1380
+ } catch (error) {
1381
+ this.logger.error('Failed to stop autonomous execution', {
1382
+ agentId: req.params.agentId,
1383
+ error: error.message
1384
+ });
1385
+
1386
+ res.status(500).json({
1387
+ success: false,
1388
+ error: error.message
1389
+ });
1390
+ }
1391
+ });
1392
+
1393
+ // Get agent mode status
1394
+ this.app.get('/api/agents/:agentId/mode', async (req, res) => {
1395
+ try {
1396
+ const { agentId } = req.params;
1397
+
1398
+ // TODO: Get session ID from query params or headers
1399
+ const request = {
1400
+ interface: INTERFACE_TYPES.WEB,
1401
+ sessionId: req.sessionID,
1402
+ action: 'get_agent_status',
1403
+ payload: { agentId }
1404
+ };
1405
+
1406
+ const response = await this.orchestrator.processRequest(request);
1407
+
1408
+ if (response.success && response.agent) {
1409
+ res.json({
1410
+ success: true,
1411
+ mode: response.agent.mode,
1412
+ modeState: response.agent.modeState,
1413
+ currentTask: response.agent.currentTask,
1414
+ iterationCount: response.agent.iterationCount,
1415
+ taskStartTime: response.agent.taskStartTime,
1416
+ stopRequested: response.agent.stopRequested
1417
+ });
1418
+ } else {
1419
+ res.status(404).json({
1420
+ success: false,
1421
+ error: 'Agent not found'
1422
+ });
1423
+ }
1424
+
1425
+ } catch (error) {
1426
+ this.logger.error('Failed to get agent mode status', {
1427
+ agentId: req.params.agentId,
1428
+ error: error.message
1429
+ });
1430
+
1431
+ res.status(500).json({
1432
+ success: false,
1433
+ error: error.message
1434
+ });
1435
+ }
1436
+ });
1437
+
1438
+ // Agent Import/Resume Endpoints
1439
+
1440
+ // Get all available agents (active + archived)
1441
+ this.app.get('/api/agents/available', async (req, res) => {
1442
+ try {
1443
+ const projectDir = this.orchestrator.config.project?.directory || process.cwd();
1444
+ const agents = await this.orchestrator.stateManager.getAllAvailableAgents(
1445
+ projectDir,
1446
+ this.orchestrator.agentPool
1447
+ );
1448
+
1449
+ res.json({
1450
+ success: true,
1451
+ agents: agents,
1452
+ total: agents.length,
1453
+ active: agents.filter(a => a.isLoaded).length,
1454
+ archived: agents.filter(a => !a.isLoaded).length
1455
+ });
1456
+ } catch (error) {
1457
+ this.logger.error('Failed to get available agents', {
1458
+ error: error.message,
1459
+ stack: error.stack
1460
+ });
1461
+
1462
+ res.status(500).json({
1463
+ success: false,
1464
+ error: error.message
1465
+ });
1466
+ }
1467
+ });
1468
+
1469
+ // Get agent metadata for preview
1470
+ this.app.get('/api/agents/:agentId/metadata', async (req, res) => {
1471
+ try {
1472
+ const { agentId } = req.params;
1473
+ const projectDir = this.orchestrator.config.project?.directory || process.cwd();
1474
+
1475
+ const metadata = await this.orchestrator.stateManager.getAgentMetadata(
1476
+ agentId,
1477
+ projectDir
1478
+ );
1479
+
1480
+ res.json({
1481
+ success: true,
1482
+ metadata
1483
+ });
1484
+ } catch (error) {
1485
+ this.logger.error('Failed to get agent metadata', {
1486
+ agentId: req.params.agentId,
1487
+ error: error.message
1488
+ });
1489
+
1490
+ res.status(404).json({
1491
+ success: false,
1492
+ error: error.message
1493
+ });
1494
+ }
1495
+ });
1496
+
1497
+ // Import archived agent
1498
+ this.app.post('/api/agents/import', async (req, res) => {
1499
+ try {
1500
+ const { agentId } = req.body;
1501
+
1502
+ if (!agentId) {
1503
+ return res.status(400).json({
1504
+ success: false,
1505
+ error: 'agentId is required in request body'
1506
+ });
1507
+ }
1508
+
1509
+ const projectDir = this.orchestrator.config.project?.directory || process.cwd();
1510
+
1511
+ // Import the agent
1512
+ const agent = await this.orchestrator.stateManager.importArchivedAgent(
1513
+ agentId,
1514
+ projectDir,
1515
+ this.orchestrator.agentPool
1516
+ );
1517
+
1518
+ // Broadcast agent added event via WebSocket
1519
+ if (this.wsManager) {
1520
+ this.wsManager.broadcast({
1521
+ type: 'agent-imported',
1522
+ agent: {
1523
+ id: agent.id,
1524
+ name: agent.name,
1525
+ status: agent.status,
1526
+ capabilities: agent.capabilities,
1527
+ model: agent.currentModel || agent.preferredModel
1528
+ }
1529
+ });
1530
+ }
1531
+
1532
+ this.logger.info('Agent imported successfully', {
1533
+ agentId: agent.id,
1534
+ name: agent.name
1535
+ });
1536
+
1537
+ res.json({
1538
+ success: true,
1539
+ agent: {
1540
+ id: agent.id,
1541
+ name: agent.name,
1542
+ status: agent.status,
1543
+ model: agent.currentModel || agent.preferredModel,
1544
+ capabilities: agent.capabilities,
1545
+ lastActivity: agent.lastActivity
1546
+ },
1547
+ message: `Agent ${agent.name} imported successfully`
1548
+ });
1549
+ } catch (error) {
1550
+ this.logger.error('Failed to import agent', {
1551
+ agentId: req.body.agentId,
1552
+ error: error.message,
1553
+ stack: error.stack
1554
+ });
1555
+
1556
+ // Determine appropriate status code
1557
+ const statusCode = error.message.includes('already active') ? 409 :
1558
+ error.message.includes('not found') ? 404 :
1559
+ error.message.includes('Invalid') ? 400 : 500;
1560
+
1561
+ res.status(statusCode).json({
1562
+ success: false,
1563
+ error: error.message
1564
+ });
1565
+ }
1566
+ });
1567
+
1568
+ // Serve React app for all other routes
1569
+ this.app.get('*', (req, res) => {
1570
+ const indexPath = path.join(__dirname, '../../web-ui/build/index.html');
1571
+ res.sendFile(indexPath, (err) => {
1572
+ if (err) {
1573
+ res.status(HTTP_STATUS.NOT_FOUND).send('Web UI not built. Run: npm run build:ui');
1574
+ }
1575
+ });
1576
+ });
1577
+ }
1578
+
1579
+ /**
1580
+ * Setup WebSocket server
1581
+ * @private
1582
+ */
1583
+ setupWebSocket() {
1584
+ this.logger.info('Setting up WebSocket server', {
1585
+ port: this.port,
1586
+ host: this.host,
1587
+ wsServerExists: !!this.wss,
1588
+ httpServerExists: !!this.server
1589
+ });
1590
+
1591
+ // Add error handler for WebSocket server
1592
+ this.wss.on('error', (error) => {
1593
+ this.logger.error('WebSocket server error:', {
1594
+ error: error.message,
1595
+ stack: error.stack,
1596
+ port: this.port
1597
+ });
1598
+ });
1599
+
1600
+ // Log when WebSocket server is ready
1601
+ this.wss.on('listening', () => {
1602
+ this.logger.info('WebSocket server is now listening', {
1603
+ port: this.port,
1604
+ host: this.host
1605
+ });
1606
+ });
1607
+
1608
+ this.wss.on('connection', (ws, req) => {
1609
+ const connectionId = `conn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1610
+
1611
+ this.logger.info('WebSocket connection established', {
1612
+ connectionId,
1613
+ ip: req.socket.remoteAddress,
1614
+ origin: req.headers.origin,
1615
+ host: req.headers.host,
1616
+ userAgent: req.headers['user-agent'],
1617
+ url: req.url
1618
+ });
1619
+
1620
+ // Store connection
1621
+ const connection = {
1622
+ id: connectionId,
1623
+ ws,
1624
+ sessionId: null,
1625
+ connectedAt: new Date().toISOString(),
1626
+ lastActivity: new Date().toISOString()
1627
+ };
1628
+
1629
+ this.connections.set(connectionId, connection);
1630
+
1631
+ // Handle messages
1632
+ ws.on('message', async (data) => {
1633
+ try {
1634
+ const message = JSON.parse(data.toString());
1635
+ await this.handleWebSocketMessage(connectionId, message);
1636
+ } catch (error) {
1637
+ this.logger.error('WebSocket message error', {
1638
+ connectionId,
1639
+ error: error.message
1640
+ });
1641
+
1642
+ ws.send(JSON.stringify({
1643
+ type: 'error',
1644
+ error: error.message
1645
+ }));
1646
+ }
1647
+ });
1648
+
1649
+ // Handle disconnect
1650
+ ws.on('close', () => {
1651
+ this.logger.info('WebSocket connection closed', { connectionId });
1652
+ this.connections.delete(connectionId);
1653
+ });
1654
+
1655
+ // Send welcome message
1656
+ ws.send(JSON.stringify({
1657
+ type: 'connected',
1658
+ connectionId,
1659
+ timestamp: new Date().toISOString()
1660
+ }));
1661
+ });
1662
+ }
1663
+
1664
+ /**
1665
+ * Handle WebSocket message
1666
+ * @private
1667
+ */
1668
+ async handleWebSocketMessage(connectionId, message) {
1669
+ const connection = this.connections.get(connectionId);
1670
+ if (!connection) return;
1671
+
1672
+ connection.lastActivity = new Date().toISOString();
1673
+
1674
+ switch (message.type) {
1675
+ case 'join_session':
1676
+ const sessionId = message.sessionId;
1677
+ connection.sessionId = sessionId;
1678
+
1679
+ this.logger.info('WebSocket joined session', {
1680
+ connectionId,
1681
+ sessionId,
1682
+ totalConnectionsForSession: Array.from(this.connections.values()).filter(c => c.sessionId === sessionId).length
1683
+ });
1684
+
1685
+ connection.ws.send(JSON.stringify({
1686
+ type: 'session_joined',
1687
+ sessionId: sessionId
1688
+ }));
1689
+ break;
1690
+
1691
+ case 'ping':
1692
+ connection.ws.send(JSON.stringify({
1693
+ type: 'pong',
1694
+ timestamp: new Date().toISOString()
1695
+ }));
1696
+ break;
1697
+
1698
+ case 'orchestrator_request':
1699
+ // Handle real-time orchestrator requests
1700
+ try {
1701
+ const request = {
1702
+ interface: INTERFACE_TYPES.WEB,
1703
+ sessionId: connection.sessionId,
1704
+ action: message.action,
1705
+ payload: message.payload,
1706
+ projectDir: message.projectDir || process.cwd()
1707
+ };
1708
+
1709
+ const response = await this.orchestrator.processRequest(request);
1710
+
1711
+ connection.ws.send(JSON.stringify({
1712
+ type: 'orchestrator_response',
1713
+ requestId: message.requestId,
1714
+ response
1715
+ }));
1716
+
1717
+ } catch (error) {
1718
+ connection.ws.send(JSON.stringify({
1719
+ type: 'error',
1720
+ requestId: message.requestId,
1721
+ error: error.message
1722
+ }));
1723
+ }
1724
+ break;
1725
+
1726
+ default:
1727
+ this.logger.warn('Unknown WebSocket message type', {
1728
+ connectionId,
1729
+ type: message.type
1730
+ });
1731
+ }
1732
+ }
1733
+
1734
+ /**
1735
+ * Broadcast message to all connections in a session
1736
+ * @private
1737
+ */
1738
+ broadcastToSession(sessionId, message) {
1739
+ const sessionConnections = Array.from(this.connections.values())
1740
+ .filter(conn => conn.sessionId === sessionId);
1741
+
1742
+ // If no connections found for this session, try broadcasting to all connections
1743
+ // This handles cases where session IDs might be mismatched
1744
+ let allConnections = [];
1745
+ if (sessionConnections.length === 0) {
1746
+ allConnections = Array.from(this.connections.values());
1747
+
1748
+ this.logger?.warn('šŸ”„ No connections for session, trying all connections:', {
1749
+ targetSessionId: sessionId,
1750
+ totalConnections: this.connections.size,
1751
+ allSessionIds: Array.from(this.connections.values()).map(c => c.sessionId).filter(Boolean)
1752
+ });
1753
+ }
1754
+
1755
+ const targetConnections = sessionConnections.length > 0 ? sessionConnections : allConnections;
1756
+
1757
+ this.logger?.info('šŸ“” WebSocket broadcastToSession called:', {
1758
+ sessionId,
1759
+ messageType: message.type,
1760
+ agentId: message.agentId,
1761
+ totalConnections: this.connections.size,
1762
+ sessionConnections: sessionConnections.length,
1763
+ targetConnections: targetConnections.length,
1764
+ connectionIds: targetConnections.map(c => c.id),
1765
+ usingFallback: sessionConnections.length === 0,
1766
+ messagePreview: message.type === 'autonomous_update' && message.message ? {
1767
+ messageId: message.message.id,
1768
+ messageRole: message.message.role,
1769
+ contentLength: message.message.content?.length,
1770
+ hasToolResults: !!message.message.toolResults
1771
+ } : undefined
1772
+ });
1773
+
1774
+ for (const connection of targetConnections) {
1775
+ try {
1776
+ const fullMessage = {
1777
+ ...message,
1778
+ timestamp: new Date().toISOString()
1779
+ };
1780
+
1781
+ connection.ws.send(JSON.stringify(fullMessage));
1782
+
1783
+ this.logger?.info('āœ… WebSocket message sent to connection:', {
1784
+ connectionId: connection.id,
1785
+ messageType: message.type,
1786
+ agentId: message.agentId
1787
+ });
1788
+ } catch (error) {
1789
+ this.logger.warn('āŒ Failed to send WebSocket message', {
1790
+ connectionId: connection.id,
1791
+ sessionId,
1792
+ messageType: message.type,
1793
+ error: error.message
1794
+ });
1795
+ }
1796
+ }
1797
+ }
1798
+
1799
+ /**
1800
+ * Start the HTTP server
1801
+ * @private
1802
+ */
1803
+ async startServer() {
1804
+ return new Promise((resolve, reject) => {
1805
+ this.server.listen(this.port, this.host, (error) => {
1806
+ if (error) {
1807
+ this.logger.error('Failed to start HTTP server', {
1808
+ error: error.message,
1809
+ port: this.port,
1810
+ host: this.host
1811
+ });
1812
+ reject(error);
1813
+ } else {
1814
+ this.isRunning = true;
1815
+
1816
+ // Verify WebSocket server status
1817
+ this.logger.info('HTTP server started successfully', {
1818
+ port: this.port,
1819
+ host: this.host,
1820
+ httpUrl: `http://${this.host}:${this.port}`,
1821
+ wsUrl: `ws://${this.host}:${this.port}`,
1822
+ wsServerAttached: !!this.wss,
1823
+ wsConnections: this.connections.size
1824
+ });
1825
+
1826
+ // Test WebSocket server availability
1827
+ setTimeout(async () => {
1828
+ await this.testWebSocketServer();
1829
+ }, 1000);
1830
+
1831
+ resolve();
1832
+ }
1833
+ });
1834
+
1835
+ // Add server error handler
1836
+ this.server.on('error', (error) => {
1837
+ this.logger.error('HTTP server error:', {
1838
+ error: error.message,
1839
+ stack: error.stack,
1840
+ port: this.port
1841
+ });
1842
+ });
1843
+ });
1844
+ }
1845
+
1846
+ /**
1847
+ * Test WebSocket server availability
1848
+ * @private
1849
+ */
1850
+ async testWebSocketServer() {
1851
+ try {
1852
+ const { default: WebSocket } = await import('ws');
1853
+ const testWs = new WebSocket(`ws://localhost:${this.port}`);
1854
+
1855
+ testWs.on('open', () => {
1856
+ this.logger.info('āœ… WebSocket server test: SUCCESSFUL', {
1857
+ port: this.port,
1858
+ url: `ws://localhost:${this.port}`
1859
+ });
1860
+ testWs.close();
1861
+ });
1862
+
1863
+ testWs.on('error', (error) => {
1864
+ this.logger.error('āŒ WebSocket server test: FAILED', {
1865
+ port: this.port,
1866
+ url: `ws://localhost:${this.port}`,
1867
+ error: error.message,
1868
+ code: error.code
1869
+ });
1870
+ });
1871
+
1872
+ testWs.on('close', (code, reason) => {
1873
+ if (code === 1000) {
1874
+ this.logger.info('WebSocket test connection closed cleanly');
1875
+ }
1876
+ });
1877
+
1878
+ // Timeout the test
1879
+ setTimeout(() => {
1880
+ if (testWs.readyState === WebSocket.CONNECTING) {
1881
+ this.logger.error('āŒ WebSocket server test: TIMEOUT', {
1882
+ port: this.port,
1883
+ url: `ws://localhost:${this.port}`,
1884
+ readyState: testWs.readyState
1885
+ });
1886
+ testWs.terminate();
1887
+ }
1888
+ }, 5000);
1889
+
1890
+ } catch (error) {
1891
+ this.logger.error('āŒ WebSocket server test: EXCEPTION', {
1892
+ error: error.message,
1893
+ stack: error.stack
1894
+ });
1895
+ }
1896
+ }
1897
+
1898
+ /**
1899
+ * Get default models when API is unavailable
1900
+ * @returns {Array} Default model list
1901
+ * @private
1902
+ */
1903
+ getDefaultModels() {
1904
+ return [
1905
+ {
1906
+ name: 'anthropic-sonnet',
1907
+ category: 'anthropic',
1908
+ type: 'chat',
1909
+ maxTokens: 10000,
1910
+ supportsVision: true,
1911
+ supportsSystem: true,
1912
+ pricing: {
1913
+ input: 0.003,
1914
+ output: 0.015,
1915
+ unit: '1K tokens'
1916
+ }
1917
+ },
1918
+ {
1919
+ name: 'anthropic-opus',
1920
+ category: 'anthropic',
1921
+ type: 'chat',
1922
+ maxTokens: 10000,
1923
+ supportsVision: true,
1924
+ supportsSystem: true,
1925
+ pricing: {
1926
+ input: 0.015,
1927
+ output: 0.075,
1928
+ unit: '1K tokens'
1929
+ }
1930
+ },
1931
+ {
1932
+ name: 'anthropic-haiku',
1933
+ category: 'anthropic',
1934
+ type: 'chat',
1935
+ maxTokens: 10000,
1936
+ supportsVision: false,
1937
+ supportsSystem: true,
1938
+ pricing: {
1939
+ input: 0.0025,
1940
+ output: 0.0125,
1941
+ unit: '1K tokens'
1942
+ }
1943
+ },
1944
+ {
1945
+ name: 'gpt-4',
1946
+ category: 'openai',
1947
+ type: 'chat',
1948
+ maxTokens: 8000,
1949
+ supportsVision: true,
1950
+ supportsSystem: true,
1951
+ pricing: {
1952
+ input: 0.03,
1953
+ output: 0.06,
1954
+ unit: '1K tokens'
1955
+ }
1956
+ },
1957
+ {
1958
+ name: 'gpt-4-mini',
1959
+ category: 'openai',
1960
+ type: 'chat',
1961
+ maxTokens: 16000,
1962
+ supportsVision: false,
1963
+ supportsSystem: true,
1964
+ pricing: {
1965
+ input: 0.0015,
1966
+ output: 0.006,
1967
+ unit: '1K tokens'
1968
+ }
1969
+ },
1970
+ {
1971
+ name: 'deepseek-r1',
1972
+ category: 'deepseek',
1973
+ type: 'chat',
1974
+ maxTokens: 8000,
1975
+ supportsVision: false,
1976
+ supportsSystem: true,
1977
+ pricing: {
1978
+ input: 0.002,
1979
+ output: 0.008,
1980
+ unit: '1K tokens'
1981
+ }
1982
+ },
1983
+ {
1984
+ name: 'phi-4',
1985
+ category: 'microsoft',
1986
+ type: 'chat',
1987
+ maxTokens: 4000,
1988
+ supportsVision: false,
1989
+ supportsSystem: true,
1990
+ pricing: {
1991
+ input: 0.001,
1992
+ output: 0.004,
1993
+ unit: '1K tokens'
1994
+ }
1995
+ }
1996
+ ];
1997
+ }
1998
+
1999
+ /**
2000
+ * Set API key manager instance
2001
+ * @param {ApiKeyManager} apiKeyManager - API key manager instance
2002
+ */
2003
+ setApiKeyManager(apiKeyManager) {
2004
+ this.apiKeyManager = apiKeyManager;
2005
+
2006
+ this.logger?.info('API key manager set for web server', {
2007
+ hasManager: !!apiKeyManager
2008
+ });
2009
+ }
2010
+
2011
+ /**
2012
+ * Extract vendor name from model name
2013
+ * @param {string} model - Model name
2014
+ * @returns {string|null} Vendor name
2015
+ * @private
2016
+ */
2017
+ _getVendorFromModel(model) {
2018
+ if (!model) return null;
2019
+
2020
+ const modelName = model.toLowerCase();
2021
+
2022
+ if (modelName.includes('anthropic') || modelName.includes('claude')) {
2023
+ return 'anthropic';
2024
+ } else if (modelName.includes('openai') || modelName.includes('gpt')) {
2025
+ return 'openai';
2026
+ } else if (modelName.includes('deepseek')) {
2027
+ return 'deepseek';
2028
+ } else if (modelName.includes('phi')) {
2029
+ return 'microsoft';
2030
+ }
2031
+
2032
+ return null;
2033
+ }
2034
+
2035
+ /**
2036
+ * Get server status
2037
+ * @returns {Object} Server status
2038
+ */
2039
+ getStatus() {
2040
+ return {
2041
+ isRunning: this.isRunning,
2042
+ port: this.port,
2043
+ host: this.host,
2044
+ connections: this.connections.size,
2045
+ sessions: this.sessions.size,
2046
+ url: `http://${this.host}:${this.port}`
2047
+ };
2048
+ }
2049
+
2050
+ /**
2051
+ * Shutdown the web server
2052
+ * @returns {Promise<void>}
2053
+ */
2054
+ async shutdown() {
2055
+ if (!this.isRunning) return;
2056
+
2057
+ this.logger.info('Shutting down web server...');
2058
+
2059
+ // Close all WebSocket connections
2060
+ for (const connection of this.connections.values()) {
2061
+ connection.ws.close();
2062
+ }
2063
+ this.connections.clear();
2064
+
2065
+ // Close WebSocket server
2066
+ this.wss.close();
2067
+
2068
+ // Close HTTP server
2069
+ return new Promise((resolve) => {
2070
+ this.server.close(() => {
2071
+ this.isRunning = false;
2072
+ this.logger.info('Web server shutdown complete');
2073
+ resolve();
2074
+ });
2075
+ });
2076
+ }
2077
+ }
2078
+
2079
+ export default WebServer;
2080
+
2081
+ // Main execution block - start server if run directly
2082
+ if (import.meta.url === `file://${process.argv[1]}`) {
2083
+ // Simple console logger for standalone mode
2084
+ const simpleLogger = {
2085
+ info: (msg, data) => console.log(`[INFO] ${msg}`, data ? JSON.stringify(data, null, 2) : ''),
2086
+ error: (msg, data) => console.error(`[ERROR] ${msg}`, data ? JSON.stringify(data, null, 2) : ''),
2087
+ warn: (msg, data) => console.warn(`[WARN] ${msg}`, data ? JSON.stringify(data, null, 2) : ''),
2088
+ debug: (msg, data) => console.log(`[DEBUG] ${msg}`, data ? JSON.stringify(data, null, 2) : '')
2089
+ };
2090
+
2091
+ // Simple orchestrator mock for standalone mode
2092
+ const mockOrchestrator = {
2093
+ processAction: async (action, data) => {
2094
+ simpleLogger.info('Mock orchestrator action', { action, data });
2095
+
2096
+ // Mock responses for different actions
2097
+ switch (action) {
2098
+ case ORCHESTRATOR_ACTIONS.LIST_AGENTS:
2099
+ return {
2100
+ success: true,
2101
+ data: []
2102
+ };
2103
+
2104
+ case ORCHESTRATOR_ACTIONS.CREATE_AGENT:
2105
+ return {
2106
+ success: true,
2107
+ data: {
2108
+ id: `agent-${Date.now()}`,
2109
+ name: data.name || 'New Agent',
2110
+ status: 'active',
2111
+ model: data.model || 'anthropic-sonnet',
2112
+ systemPrompt: data.systemPrompt || 'You are a helpful AI assistant.'
2113
+ }
2114
+ };
2115
+
2116
+ case ORCHESTRATOR_ACTIONS.SEND_MESSAGE:
2117
+ return {
2118
+ success: true,
2119
+ data: {
2120
+ message: {
2121
+ id: `msg-${Date.now()}`,
2122
+ content: `Echo: ${data.message}`,
2123
+ timestamp: new Date().toISOString()
2124
+ }
2125
+ }
2126
+ };
2127
+
2128
+ default:
2129
+ return {
2130
+ success: false,
2131
+ error: `Unknown action: ${action}`
2132
+ };
2133
+ }
2134
+ }
2135
+ };
2136
+
2137
+ const server = new WebServer(mockOrchestrator, simpleLogger, {
2138
+ port: 8080,
2139
+ host: '0.0.0.0'
2140
+ });
2141
+
2142
+ console.log('šŸš€ Starting Loxia Web Server in standalone mode...');
2143
+
2144
+ server.startServer()
2145
+ .then(() => {
2146
+ const status = server.getStatus();
2147
+ console.log(`āœ… Web Server running at ${status.url}`);
2148
+ console.log('šŸ“± Web UI available at: http://localhost:3001 (if running)');
2149
+ console.log('šŸ”§ API available at: http://localhost:8080/api');
2150
+ })
2151
+ .catch(error => {
2152
+ console.error('āŒ Failed to start web server:', error.message);
2153
+ process.exit(1);
2154
+ });
2155
+
2156
+ // Graceful shutdown
2157
+ process.on('SIGINT', async () => {
2158
+ console.log('\nšŸ›‘ Shutting down web server...');
2159
+ await server.shutdown();
2160
+ process.exit(0);
2161
+ });
2162
+ }