@chinchillaenterprises/mcp-dev-logger 2.1.0 → 2.3.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.
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import { writeFileSync, readFileSync, existsSync, appendFileSync, copyFileSync,
8
8
  import { join, resolve } from "path";
9
9
  import { tmpdir } from "os";
10
10
  import { promisify } from "util";
11
+ import { chromium } from "playwright";
11
12
  const execAsync = promisify(exec);
12
13
  // Schema definitions
13
14
  const StartLogStreamingArgsSchema = z.object({
@@ -47,6 +48,16 @@ const CheckRunningProcessesArgsSchema = z.object({
47
48
  const DiscoverLogsArgsSchema = z.object({
48
49
  sessionDate: z.string().optional().describe("Specific session date (YYYY-MM-DD) to discover logs for (defaults to most recent)")
49
50
  });
51
+ const StartFrontendWithBrowserArgsSchema = z.object({
52
+ command: z.string().optional().describe("Dev command to run (default: npm run dev)"),
53
+ port: z.number().optional().describe("Port to wait for (default: auto-detect from output)"),
54
+ waitTimeout: z.number().optional().describe("Max time to wait for server in ms (default: 30000)"),
55
+ browserDelay: z.number().optional().describe("Delay after server ready in ms (default: 1000)"),
56
+ teachingMode: z.boolean().optional().describe("Enable teaching features (default: true)"),
57
+ processId: z.string().optional().describe("Custom process ID (default: frontend-with-browser)"),
58
+ env: z.record(z.string()).optional().describe("Environment variables"),
59
+ cwd: z.string().optional().describe("Working directory")
60
+ });
50
61
  class DevLoggerServer {
51
62
  server;
52
63
  activeServers;
@@ -438,9 +449,13 @@ class DevLoggerServer {
438
449
  console.error('Log cleanup error:', error);
439
450
  }
440
451
  }
441
- killAllProcesses() {
452
+ async killAllProcesses() {
442
453
  for (const [id, info] of this.activeServers) {
443
454
  try {
455
+ // Close browser if running
456
+ if (info.browser) {
457
+ await info.browser.close();
458
+ }
444
459
  if (info.process) {
445
460
  // Remove all event listeners to prevent memory leaks
446
461
  info.process.stdout?.removeAllListeners('data');
@@ -633,6 +648,93 @@ class DevLoggerServer {
633
648
  }
634
649
  }
635
650
  }
651
+ },
652
+ {
653
+ name: "dev_launch_test_browser",
654
+ description: "Launch a TEST BROWSER for students to interact with. All console logs from this browser are captured automatically. Students should use THIS browser (not their regular browser) to test their app.",
655
+ inputSchema: {
656
+ type: "object",
657
+ properties: {
658
+ processId: {
659
+ type: "string",
660
+ description: "Process ID to attach browser console to"
661
+ },
662
+ browserUrl: {
663
+ type: "string",
664
+ description: "URL to open in test browser (default: http://localhost:3000)"
665
+ },
666
+ teachingMode: {
667
+ type: "boolean",
668
+ description: "Enable teaching mode with DevTools open and slower actions (default: true)"
669
+ },
670
+ viewport: {
671
+ type: "object",
672
+ properties: {
673
+ width: { type: "number", default: 1280 },
674
+ height: { type: "number", default: 800 }
675
+ },
676
+ description: "Browser window size"
677
+ },
678
+ highlightErrors: {
679
+ type: "boolean",
680
+ description: "Add visual indicator when console errors occur (default: true)"
681
+ }
682
+ }
683
+ }
684
+ },
685
+ {
686
+ name: "dev_stop_browser_console",
687
+ description: "Stop browser console capture for a specific process",
688
+ inputSchema: {
689
+ type: "object",
690
+ properties: {
691
+ processId: {
692
+ type: "string",
693
+ description: "Process ID to stop browser console capture for"
694
+ }
695
+ }
696
+ }
697
+ },
698
+ {
699
+ name: "dev_start_frontend_with_browser",
700
+ description: "🚀 ONE-CLICK STUDENT WORKFLOW: Start frontend server AND automatically launch test browser when ready. Perfect for teaching environments - reduces setup from 3 commands to 1. Browser launches with DevTools open, error highlighting, and all console logs captured.",
701
+ inputSchema: {
702
+ type: "object",
703
+ properties: {
704
+ command: {
705
+ type: "string",
706
+ description: "Dev command to run (default: npm run dev)"
707
+ },
708
+ port: {
709
+ type: "number",
710
+ description: "Port to wait for (default: auto-detect from output)"
711
+ },
712
+ waitTimeout: {
713
+ type: "number",
714
+ description: "Max time to wait for server in ms (default: 30000)"
715
+ },
716
+ browserDelay: {
717
+ type: "number",
718
+ description: "Delay after server ready in ms (default: 1000)"
719
+ },
720
+ teachingMode: {
721
+ type: "boolean",
722
+ description: "Enable teaching features (default: true)"
723
+ },
724
+ processId: {
725
+ type: "string",
726
+ description: "Custom process ID (default: frontend-with-browser)"
727
+ },
728
+ env: {
729
+ type: "object",
730
+ description: "Environment variables"
731
+ },
732
+ cwd: {
733
+ type: "string",
734
+ description: "Working directory"
735
+ }
736
+ }
737
+ }
636
738
  }
637
739
  ];
638
740
  return { tools };
@@ -843,6 +945,10 @@ class DevLoggerServer {
843
945
  };
844
946
  }
845
947
  try {
948
+ // Close browser if running
949
+ if (serverInfo.browser) {
950
+ await serverInfo.browser.close();
951
+ }
846
952
  // Remove all event listeners to prevent memory leaks
847
953
  if (serverInfo.process) {
848
954
  serverInfo.process.stdout?.removeAllListeners('data');
@@ -890,6 +996,10 @@ class DevLoggerServer {
890
996
  const stoppedProcesses = [];
891
997
  for (const [processId, serverInfo] of this.activeServers) {
892
998
  try {
999
+ // Close browser if running
1000
+ if (serverInfo.browser) {
1001
+ await serverInfo.browser.close();
1002
+ }
893
1003
  // Remove all event listeners to prevent memory leaks
894
1004
  if (serverInfo.process) {
895
1005
  serverInfo.process.stdout?.removeAllListeners('data');
@@ -1233,6 +1343,707 @@ class DevLoggerServer {
1233
1343
  throw new Error(`Failed to discover logs: ${error instanceof Error ? error.message : String(error)}`);
1234
1344
  }
1235
1345
  }
1346
+ case "dev_launch_test_browser": {
1347
+ const validatedArgs = z.object({
1348
+ processId: z.string().optional(),
1349
+ browserUrl: z.string().optional(),
1350
+ teachingMode: z.boolean().optional(),
1351
+ viewport: z.object({
1352
+ width: z.number().default(1280),
1353
+ height: z.number().default(800)
1354
+ }).optional(),
1355
+ highlightErrors: z.boolean().optional()
1356
+ }).parse(args);
1357
+ try {
1358
+ // Find the process to attach to
1359
+ const processId = this.findProcessOrDefault(validatedArgs.processId);
1360
+ if (!processId) {
1361
+ return {
1362
+ content: [
1363
+ {
1364
+ type: "text",
1365
+ text: JSON.stringify({
1366
+ status: "error",
1367
+ message: "No dev server process found. Start a dev server first with dev_start_log_streaming"
1368
+ }, null, 2)
1369
+ }
1370
+ ]
1371
+ };
1372
+ }
1373
+ const serverInfo = this.activeServers.get(processId);
1374
+ // Check if browser already running for this process
1375
+ if (serverInfo.browser) {
1376
+ return {
1377
+ content: [
1378
+ {
1379
+ type: "text",
1380
+ text: JSON.stringify({
1381
+ status: "already_running",
1382
+ message: `Browser console capture already active for process '${processId}'`
1383
+ }, null, 2)
1384
+ }
1385
+ ]
1386
+ };
1387
+ }
1388
+ // Launch browser with student-friendly defaults
1389
+ const teachingMode = validatedArgs.teachingMode !== false;
1390
+ const viewport = validatedArgs.viewport || { width: 1280, height: 800 };
1391
+ const browser = await chromium.launch({
1392
+ headless: false, // Always visible for students
1393
+ slowMo: teachingMode ? 50 : 0, // Slow down actions in teaching mode
1394
+ devtools: teachingMode, // Open DevTools in teaching mode
1395
+ args: [
1396
+ `--window-size=${viewport.width},${viewport.height}`,
1397
+ '--window-position=100,100',
1398
+ '--disable-features=RendererCodeIntegrity' // Prevent some security warnings
1399
+ ]
1400
+ });
1401
+ const context = await browser.newContext({
1402
+ viewport: viewport,
1403
+ ignoreHTTPSErrors: true // Common in dev environments
1404
+ });
1405
+ const page = await context.newPage();
1406
+ // Add page title to identify this as the test browser
1407
+ await page.evaluate(() => {
1408
+ // @ts-ignore - browser context
1409
+ document.title = '🧪 TEST BROWSER - Console Logs Captured';
1410
+ });
1411
+ // Set up console event handler with student-friendly features
1412
+ const highlightErrors = validatedArgs.highlightErrors !== false;
1413
+ page.on('console', (msg) => {
1414
+ const msgType = msg.type();
1415
+ const timestamp = new Date().toISOString();
1416
+ const args = msg.args();
1417
+ // Format console message
1418
+ let messageText = msg.text();
1419
+ // Visual feedback for errors in teaching mode
1420
+ if (highlightErrors && msgType === 'error') {
1421
+ page.evaluate(() => {
1422
+ // @ts-ignore - browser context
1423
+ const flash = document.createElement('div');
1424
+ flash.style.cssText = `
1425
+ position: fixed;
1426
+ top: 0;
1427
+ left: 0;
1428
+ right: 0;
1429
+ bottom: 0;
1430
+ border: 5px solid red;
1431
+ pointer-events: none;
1432
+ z-index: 999999;
1433
+ animation: flash 0.5s ease-in-out;
1434
+ `;
1435
+ flash.innerHTML = `
1436
+ <div style="
1437
+ background: red;
1438
+ color: white;
1439
+ padding: 10px;
1440
+ position: absolute;
1441
+ top: 0;
1442
+ left: 0;
1443
+ right: 0;
1444
+ text-align: center;
1445
+ font-family: monospace;
1446
+ ">⚠️ Console Error Detected - Check Logs!</div>
1447
+ `;
1448
+ // @ts-ignore - browser context
1449
+ document.body.appendChild(flash);
1450
+ setTimeout(() => flash.remove(), 2000);
1451
+ }).catch(() => { });
1452
+ }
1453
+ // Try to get better formatting for objects
1454
+ if (args.length > 0) {
1455
+ Promise.all(args.map(arg => arg.jsonValue().catch(() => arg.toString())))
1456
+ .then(values => {
1457
+ const formattedMsg = values.map(v => typeof v === 'object' ? JSON.stringify(v, null, 2) : String(v)).join(' ');
1458
+ const logEntry = `[${timestamp}] [${processId}] [BROWSER] [${msgType.toUpperCase()}] ${formattedMsg}\n`;
1459
+ appendFileSync(serverInfo.outputFile, logEntry);
1460
+ // Also log to terminal for immediate feedback
1461
+ if (msgType === 'error' || msgType === 'warning') {
1462
+ console.error(`[STUDENT BROWSER] ${msgType === 'warning' ? 'WARN' : msgType.toUpperCase()}: ${formattedMsg}`);
1463
+ }
1464
+ })
1465
+ .catch(() => {
1466
+ // Fallback to simple text
1467
+ const logEntry = `[${timestamp}] [${processId}] [BROWSER] [${msgType.toUpperCase()}] ${messageText}\n`;
1468
+ appendFileSync(serverInfo.outputFile, logEntry);
1469
+ });
1470
+ }
1471
+ else {
1472
+ const logEntry = `[${timestamp}] [${processId}] [BROWSER] [${msgType.toUpperCase()}] ${messageText}\n`;
1473
+ appendFileSync(serverInfo.outputFile, logEntry);
1474
+ }
1475
+ });
1476
+ // Handle page errors (including hydration errors)
1477
+ page.on('pageerror', (error) => {
1478
+ const timestamp = new Date().toISOString();
1479
+ const logEntry = `[${timestamp}] [${processId}] [BROWSER] [PAGE ERROR] ${error.message}\n${error.stack}\n`;
1480
+ appendFileSync(serverInfo.outputFile, logEntry);
1481
+ // Visual feedback for page errors
1482
+ page.evaluate(() => {
1483
+ // @ts-ignore - browser context
1484
+ const errorBanner = document.createElement('div');
1485
+ errorBanner.style.cssText = `
1486
+ position: fixed;
1487
+ top: 40px;
1488
+ left: 50%;
1489
+ transform: translateX(-50%);
1490
+ background: #dc2626;
1491
+ color: white;
1492
+ padding: 12px 24px;
1493
+ border-radius: 6px;
1494
+ font-family: monospace;
1495
+ font-size: 14px;
1496
+ z-index: 999999;
1497
+ box-shadow: 0 4px 6px rgba(0,0,0,0.3);
1498
+ animation: slideDown 0.3s ease-out;
1499
+ `;
1500
+ errorBanner.textContent = '⚠️ JavaScript Error - Check Console & Logs';
1501
+ // @ts-ignore - browser context
1502
+ const style = document.createElement('style');
1503
+ style.textContent = `
1504
+ @keyframes slideDown {
1505
+ from { transform: translate(-50%, -100%); opacity: 0; }
1506
+ to { transform: translate(-50%, 0); opacity: 1; }
1507
+ }
1508
+ `;
1509
+ // @ts-ignore - browser context
1510
+ document.head.appendChild(style);
1511
+ // @ts-ignore - browser context
1512
+ document.body.appendChild(errorBanner);
1513
+ setTimeout(() => errorBanner.remove(), 5000);
1514
+ }).catch(() => { });
1515
+ });
1516
+ // Handle unhandled promise rejections
1517
+ await page.addInitScript(() => {
1518
+ // @ts-ignore - browser context
1519
+ window.addEventListener('unhandledrejection', (event) => {
1520
+ console.error('Unhandled Promise Rejection:', event.reason);
1521
+ });
1522
+ });
1523
+ // Handle network response errors
1524
+ page.on('response', (response) => {
1525
+ if (response.status() >= 400) {
1526
+ const timestamp = new Date().toISOString();
1527
+ const logEntry = `[${timestamp}] [${processId}] [BROWSER] [NETWORK ERROR] ${response.status()} ${response.statusText()} - ${response.url()}\n`;
1528
+ appendFileSync(serverInfo.outputFile, logEntry);
1529
+ }
1530
+ });
1531
+ // Navigate to URL
1532
+ const browserUrl = validatedArgs.browserUrl || "http://localhost:3000";
1533
+ try {
1534
+ await page.goto(browserUrl, { waitUntil: 'domcontentloaded' });
1535
+ // Add student instruction banner
1536
+ if (teachingMode) {
1537
+ await page.evaluate(() => {
1538
+ // @ts-ignore - browser context
1539
+ const banner = document.createElement('div');
1540
+ banner.id = 'test-browser-banner';
1541
+ banner.style.cssText = `
1542
+ position: fixed;
1543
+ top: 0;
1544
+ left: 0;
1545
+ right: 0;
1546
+ background: #4CAF50;
1547
+ color: white;
1548
+ padding: 10px;
1549
+ text-align: center;
1550
+ z-index: 99999;
1551
+ font-family: monospace;
1552
+ font-size: 14px;
1553
+ box-shadow: 0 2px 5px rgba(0,0,0,0.2);
1554
+ `;
1555
+ banner.innerHTML = `
1556
+ 🧪 TEST BROWSER - All console logs are being captured!
1557
+ <button onclick="this.parentElement.style.display='none'" style="
1558
+ margin-left: 20px;
1559
+ background: transparent;
1560
+ border: 1px solid white;
1561
+ color: white;
1562
+ padding: 2px 10px;
1563
+ cursor: pointer;
1564
+ ">Hide</button>
1565
+ `;
1566
+ // @ts-ignore - browser context
1567
+ document.body.prepend(banner);
1568
+ }).catch(() => { });
1569
+ }
1570
+ }
1571
+ catch (error) {
1572
+ const timestamp = new Date().toISOString();
1573
+ const logEntry = `[${timestamp}] [${processId}] [BROWSER] [NAVIGATION ERROR] Failed to navigate to ${browserUrl}: ${error}\n`;
1574
+ appendFileSync(serverInfo.outputFile, logEntry);
1575
+ }
1576
+ // Update server info
1577
+ serverInfo.browser = browser;
1578
+ serverInfo.page = page;
1579
+ serverInfo.browserUrl = browserUrl;
1580
+ serverInfo.consoleCapture = true;
1581
+ // Log browser start
1582
+ const timestamp = new Date().toISOString();
1583
+ const logEntry = `[${timestamp}] [${processId}] [BROWSER] Console capture started for ${browserUrl}\n`;
1584
+ appendFileSync(serverInfo.outputFile, logEntry);
1585
+ return {
1586
+ content: [
1587
+ {
1588
+ type: "text",
1589
+ text: JSON.stringify({
1590
+ status: "started",
1591
+ processId: processId,
1592
+ browserUrl: browserUrl,
1593
+ teachingMode: teachingMode,
1594
+ message: `🧪 TEST BROWSER LAUNCHED!\n\n` +
1595
+ `👉 IMPORTANT: Students should use THIS browser window (not their regular browser)\n` +
1596
+ `📝 All console logs are being captured to: ${serverInfo.outputFile}\n` +
1597
+ `🔍 DevTools is ${teachingMode ? 'OPEN' : 'CLOSED'} for debugging\n` +
1598
+ `⚠️ Errors will ${highlightErrors ? 'flash red' : 'not flash'} on screen\n\n` +
1599
+ `Students can now interact with the app at: ${browserUrl}`
1600
+ }, null, 2)
1601
+ }
1602
+ ]
1603
+ };
1604
+ }
1605
+ catch (error) {
1606
+ throw new Error(`Failed to start browser console capture: ${error instanceof Error ? error.message : String(error)}`);
1607
+ }
1608
+ }
1609
+ case "dev_stop_browser_console": {
1610
+ const validatedArgs = z.object({
1611
+ processId: z.string().optional()
1612
+ }).parse(args);
1613
+ try {
1614
+ const processId = this.findProcessOrDefault(validatedArgs.processId);
1615
+ if (!processId) {
1616
+ return {
1617
+ content: [
1618
+ {
1619
+ type: "text",
1620
+ text: JSON.stringify({
1621
+ status: "error",
1622
+ message: "No process found"
1623
+ }, null, 2)
1624
+ }
1625
+ ]
1626
+ };
1627
+ }
1628
+ const serverInfo = this.activeServers.get(processId);
1629
+ if (!serverInfo || !serverInfo.browser) {
1630
+ return {
1631
+ content: [
1632
+ {
1633
+ type: "text",
1634
+ text: JSON.stringify({
1635
+ status: "not_running",
1636
+ message: `No browser console capture running for process '${processId}'`
1637
+ }, null, 2)
1638
+ }
1639
+ ]
1640
+ };
1641
+ }
1642
+ // Close browser
1643
+ await serverInfo.browser.close();
1644
+ // Clear browser info
1645
+ serverInfo.browser = undefined;
1646
+ serverInfo.page = undefined;
1647
+ serverInfo.browserUrl = undefined;
1648
+ serverInfo.consoleCapture = false;
1649
+ // Log browser stop
1650
+ const timestamp = new Date().toISOString();
1651
+ const logEntry = `[${timestamp}] [${processId}] [BROWSER] Console capture stopped\n`;
1652
+ appendFileSync(serverInfo.outputFile, logEntry);
1653
+ return {
1654
+ content: [
1655
+ {
1656
+ type: "text",
1657
+ text: JSON.stringify({
1658
+ status: "stopped",
1659
+ processId: processId,
1660
+ message: `Browser console capture stopped for process '${processId}'`
1661
+ }, null, 2)
1662
+ }
1663
+ ]
1664
+ };
1665
+ }
1666
+ catch (error) {
1667
+ throw new Error(`Failed to stop browser console capture: ${error instanceof Error ? error.message : String(error)}`);
1668
+ }
1669
+ }
1670
+ case "dev_start_frontend_with_browser": {
1671
+ const validatedArgs = StartFrontendWithBrowserArgsSchema.parse(args);
1672
+ // Defaults
1673
+ const command = validatedArgs.command || "npm run dev";
1674
+ const processId = validatedArgs.processId || "frontend-with-browser";
1675
+ const cwd = resolve(validatedArgs.cwd || process.cwd());
1676
+ const waitTimeout = validatedArgs.waitTimeout || 30000;
1677
+ const browserDelay = validatedArgs.browserDelay || 1000;
1678
+ const teachingMode = validatedArgs.teachingMode !== false; // default true
1679
+ try {
1680
+ // Check if process already exists
1681
+ if (this.activeServers.has(processId)) {
1682
+ throw new Error(`Process '${processId}' is already running. Stop it first or use a different processId.`);
1683
+ }
1684
+ // Step 1: Start the dev server
1685
+ const outputFile = this.createStructuredLogPath(command, undefined, "frontend");
1686
+ this.cleanupOldLogs(3);
1687
+ const [program, ...cmdArgs] = command.split(' ');
1688
+ // Initial log entry
1689
+ writeFileSync(outputFile, `[${new Date().toISOString()}] Starting: ${command} (Process ID: ${processId})\n`);
1690
+ appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ✅ Starting development server...\n`);
1691
+ // Spawn the process
1692
+ const devProcess = spawn(program, cmdArgs, {
1693
+ cwd: cwd,
1694
+ env: { ...process.env, ...validatedArgs.env },
1695
+ detached: true,
1696
+ stdio: ['ignore', 'pipe', 'pipe']
1697
+ });
1698
+ const serverInfo = {
1699
+ process: devProcess,
1700
+ command: command,
1701
+ cwd: cwd,
1702
+ outputFile: outputFile,
1703
+ startTime: new Date(),
1704
+ pid: devProcess.pid,
1705
+ processId: processId
1706
+ };
1707
+ this.activeServers.set(processId, serverInfo);
1708
+ // Variables for port detection
1709
+ let detectedPort = validatedArgs.port || null;
1710
+ let serverReady = false;
1711
+ let outputBuffer = "";
1712
+ // Common port patterns
1713
+ const portPatterns = [
1714
+ /(?:Local|Listening|ready).*?(\d{4})/i,
1715
+ /localhost:(\d{4})/,
1716
+ /port\s*:?\s*(\d{4})/i,
1717
+ /on\s+http:\/\/localhost:(\d{4})/i,
1718
+ /Server running at.*?:(\d{4})/i
1719
+ ];
1720
+ // Ready indicators
1721
+ const readyPatterns = [
1722
+ /Local:\s*http:\/\/localhost/i,
1723
+ /ready on http:\/\/localhost/i,
1724
+ /Server running at/i,
1725
+ /Listening on port/i,
1726
+ /started server on/i,
1727
+ /Ready in \d+ms/i,
1728
+ /compiled successfully/i
1729
+ ];
1730
+ // Create promise for server ready detection
1731
+ const serverReadyPromise = new Promise((resolve, reject) => {
1732
+ const timeout = setTimeout(() => {
1733
+ reject(new Error(`Server did not start within ${waitTimeout}ms`));
1734
+ }, waitTimeout);
1735
+ const checkOutput = (data) => {
1736
+ const text = data.toString();
1737
+ outputBuffer += text;
1738
+ // Check for port if not detected
1739
+ if (!detectedPort) {
1740
+ for (const pattern of portPatterns) {
1741
+ const match = outputBuffer.match(pattern);
1742
+ if (match && match[1]) {
1743
+ detectedPort = parseInt(match[1]);
1744
+ appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] 🔍 Detected port: ${detectedPort}\n`);
1745
+ break;
1746
+ }
1747
+ }
1748
+ }
1749
+ // Check for ready state
1750
+ if (!serverReady) {
1751
+ for (const pattern of readyPatterns) {
1752
+ if (pattern.test(outputBuffer)) {
1753
+ serverReady = true;
1754
+ clearTimeout(timeout);
1755
+ // If we have a port, resolve immediately
1756
+ if (detectedPort) {
1757
+ resolve(detectedPort);
1758
+ }
1759
+ else {
1760
+ // Try common ports
1761
+ const commonPorts = [3000, 3001, 5173, 8080, 4200];
1762
+ detectedPort = commonPorts[0]; // Default to 3000
1763
+ appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ⚠️ Could not detect port, defaulting to ${detectedPort}\n`);
1764
+ resolve(detectedPort);
1765
+ }
1766
+ break;
1767
+ }
1768
+ }
1769
+ }
1770
+ };
1771
+ // Attach listeners
1772
+ devProcess.stdout?.on('data', checkOutput);
1773
+ devProcess.stderr?.on('data', checkOutput);
1774
+ });
1775
+ // Stream stdout to file
1776
+ devProcess.stdout?.on('data', (data) => {
1777
+ const timestamp = new Date().toISOString();
1778
+ const logEntry = `[${timestamp}] [${processId}] ${data}`;
1779
+ appendFileSync(outputFile, logEntry);
1780
+ });
1781
+ // Stream stderr to file
1782
+ devProcess.stderr?.on('data', (data) => {
1783
+ const timestamp = new Date().toISOString();
1784
+ const logEntry = `[${timestamp}] [${processId}] [ERROR] ${data}`;
1785
+ appendFileSync(outputFile, logEntry);
1786
+ });
1787
+ // Handle process exit
1788
+ devProcess.on('exit', (code, signal) => {
1789
+ const timestamp = new Date().toISOString();
1790
+ const exitMessage = `[${timestamp}] [${processId}] Process exited with code ${code} and signal ${signal}\n`;
1791
+ appendFileSync(outputFile, exitMessage);
1792
+ this.activeServers.delete(processId);
1793
+ this.saveState();
1794
+ });
1795
+ // Handle process errors
1796
+ devProcess.on('error', (error) => {
1797
+ const timestamp = new Date().toISOString();
1798
+ const errorMessage = `[${timestamp}] [${processId}] Process error: ${error.message}\n`;
1799
+ appendFileSync(outputFile, errorMessage);
1800
+ });
1801
+ this.saveState();
1802
+ // Step 2: Wait for server to be ready
1803
+ let port;
1804
+ try {
1805
+ port = await serverReadyPromise;
1806
+ appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ✅ Server ready on http://localhost:${port}\n`);
1807
+ }
1808
+ catch (error) {
1809
+ // Server didn't start properly, clean up
1810
+ const failedServerInfo = this.activeServers.get(processId);
1811
+ if (failedServerInfo) {
1812
+ try {
1813
+ // Close browser if it exists
1814
+ if (failedServerInfo.browser) {
1815
+ await failedServerInfo.browser.close();
1816
+ }
1817
+ // Remove all event listeners to prevent memory leaks
1818
+ if (failedServerInfo.process) {
1819
+ failedServerInfo.process.stdout?.removeAllListeners('data');
1820
+ failedServerInfo.process.stderr?.removeAllListeners('data');
1821
+ failedServerInfo.process.removeAllListeners('exit');
1822
+ failedServerInfo.process.removeAllListeners('error');
1823
+ }
1824
+ // Kill the process
1825
+ if (failedServerInfo.pid) {
1826
+ process.kill(failedServerInfo.pid, 'SIGTERM');
1827
+ }
1828
+ }
1829
+ catch (killError) {
1830
+ // Ignore errors during cleanup
1831
+ }
1832
+ this.activeServers.delete(processId);
1833
+ this.saveState();
1834
+ }
1835
+ throw error;
1836
+ }
1837
+ // Step 3: Wait a bit more for stability
1838
+ appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ⏳ Waiting ${browserDelay}ms before launching browser...\n`);
1839
+ await new Promise(resolve => setTimeout(resolve, browserDelay));
1840
+ // Step 4: Launch the test browser
1841
+ try {
1842
+ appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ✅ Launching test browser...\n`);
1843
+ const browser = await chromium.launch({
1844
+ headless: false,
1845
+ slowMo: teachingMode ? 50 : 0,
1846
+ devtools: teachingMode,
1847
+ args: ['--window-size=1280,800', '--window-position=100,100']
1848
+ });
1849
+ const context = await browser.newContext({
1850
+ viewport: { width: 1280, height: 800 }
1851
+ });
1852
+ const page = await context.newPage();
1853
+ // Add banner to identify test browser
1854
+ await page.addInitScript(() => {
1855
+ // @ts-ignore - browser context
1856
+ const banner = document.createElement('div');
1857
+ banner.id = 'test-browser-banner';
1858
+ banner.innerHTML = '🧪 TEST BROWSER - All console logs are being captured 📝';
1859
+ banner.style.cssText = `
1860
+ position: fixed;
1861
+ top: 0;
1862
+ left: 0;
1863
+ right: 0;
1864
+ background: #22c55e;
1865
+ color: white;
1866
+ padding: 8px;
1867
+ text-align: center;
1868
+ font-family: monospace;
1869
+ font-size: 14px;
1870
+ z-index: 999999;
1871
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
1872
+ `;
1873
+ // @ts-ignore - browser context
1874
+ if (document.readyState === 'loading') {
1875
+ // @ts-ignore - browser context
1876
+ document.addEventListener('DOMContentLoaded', () => {
1877
+ // @ts-ignore - browser context
1878
+ document.body.insertBefore(banner, document.body.firstChild);
1879
+ });
1880
+ }
1881
+ else {
1882
+ // @ts-ignore - browser context
1883
+ document.body.insertBefore(banner, document.body.firstChild);
1884
+ }
1885
+ });
1886
+ // Set up console capture
1887
+ page.on('console', (msg) => {
1888
+ const timestamp = new Date().toISOString();
1889
+ const msgType = msg.type().toUpperCase();
1890
+ const msgText = msg.text();
1891
+ const logEntry = `[${timestamp}] [${processId}] [BROWSER] [${msgType}] ${msgText}\n`;
1892
+ appendFileSync(outputFile, logEntry);
1893
+ // Flash red on errors
1894
+ if (msgType === 'ERROR') {
1895
+ page.evaluate(() => {
1896
+ // @ts-ignore - browser context
1897
+ const flash = document.createElement('div');
1898
+ flash.style.cssText = `
1899
+ position: fixed;
1900
+ top: 0;
1901
+ left: 0;
1902
+ right: 0;
1903
+ bottom: 0;
1904
+ background: rgba(239, 68, 68, 0.3);
1905
+ z-index: 999998;
1906
+ pointer-events: none;
1907
+ animation: errorFlash 0.5s ease-out;
1908
+ `;
1909
+ // @ts-ignore - browser context
1910
+ const style = document.createElement('style');
1911
+ style.textContent = `
1912
+ @keyframes errorFlash {
1913
+ 0% { opacity: 1; }
1914
+ 100% { opacity: 0; }
1915
+ }
1916
+ `;
1917
+ // @ts-ignore - browser context
1918
+ document.head.appendChild(style);
1919
+ // @ts-ignore - browser context
1920
+ document.body.appendChild(flash);
1921
+ setTimeout(() => flash.remove(), 500);
1922
+ }).catch(() => { }); // Ignore errors from flashing
1923
+ }
1924
+ });
1925
+ // Handle page errors (including hydration errors)
1926
+ page.on('pageerror', (error) => {
1927
+ const timestamp = new Date().toISOString();
1928
+ const logEntry = `[${timestamp}] [${processId}] [BROWSER] [PAGE ERROR] ${error.message}\n${error.stack}\n`;
1929
+ appendFileSync(outputFile, logEntry);
1930
+ // Visual feedback for page errors
1931
+ page.evaluate(() => {
1932
+ // @ts-ignore - browser context
1933
+ const errorBanner = document.createElement('div');
1934
+ errorBanner.style.cssText = `
1935
+ position: fixed;
1936
+ top: 40px;
1937
+ left: 50%;
1938
+ transform: translateX(-50%);
1939
+ background: #dc2626;
1940
+ color: white;
1941
+ padding: 12px 24px;
1942
+ border-radius: 6px;
1943
+ font-family: monospace;
1944
+ font-size: 14px;
1945
+ z-index: 999999;
1946
+ box-shadow: 0 4px 6px rgba(0,0,0,0.3);
1947
+ animation: slideDown 0.3s ease-out;
1948
+ `;
1949
+ errorBanner.textContent = '⚠️ JavaScript Error - Check Console & Logs';
1950
+ // @ts-ignore - browser context
1951
+ const style = document.createElement('style');
1952
+ style.textContent = `
1953
+ @keyframes slideDown {
1954
+ from { transform: translate(-50%, -100%); opacity: 0; }
1955
+ to { transform: translate(-50%, 0); opacity: 1; }
1956
+ }
1957
+ `;
1958
+ // @ts-ignore - browser context
1959
+ document.head.appendChild(style);
1960
+ // @ts-ignore - browser context
1961
+ document.body.appendChild(errorBanner);
1962
+ setTimeout(() => errorBanner.remove(), 5000);
1963
+ }).catch(() => { });
1964
+ });
1965
+ // Handle unhandled promise rejections
1966
+ await page.addInitScript(() => {
1967
+ // @ts-ignore - browser context
1968
+ window.addEventListener('unhandledrejection', (event) => {
1969
+ console.error('Unhandled Promise Rejection:', event.reason);
1970
+ });
1971
+ });
1972
+ // Handle network response errors
1973
+ page.on('response', (response) => {
1974
+ if (response.status() >= 400) {
1975
+ const timestamp = new Date().toISOString();
1976
+ const logEntry = `[${timestamp}] [${processId}] [BROWSER] [NETWORK ERROR] ${response.status()} ${response.statusText()} - ${response.url()}\n`;
1977
+ appendFileSync(outputFile, logEntry);
1978
+ }
1979
+ });
1980
+ // Navigate to the app
1981
+ const browserUrl = `http://localhost:${port}`;
1982
+ await page.goto(browserUrl, { waitUntil: 'domcontentloaded' });
1983
+ // Update server info with browser details
1984
+ serverInfo.browser = browser;
1985
+ serverInfo.page = page;
1986
+ serverInfo.browserUrl = browserUrl;
1987
+ serverInfo.consoleCapture = true;
1988
+ // Log success
1989
+ const successMessage = `
1990
+ 🎯 TEST BROWSER LAUNCHED!
1991
+
1992
+ 👉 Use the browser window that just opened (with green banner)
1993
+ 📝 All console logs are being saved
1994
+ 🔍 DevTools is open for debugging
1995
+ ⚠️ Errors will flash red on screen
1996
+
1997
+ Happy debugging! 🚀
1998
+ `;
1999
+ appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] [BROWSER] Console capture started for ${browserUrl}\n`);
2000
+ appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ${successMessage}\n`);
2001
+ return {
2002
+ content: [
2003
+ {
2004
+ type: "text",
2005
+ text: JSON.stringify({
2006
+ status: "success",
2007
+ message: successMessage.trim(),
2008
+ processId: processId,
2009
+ serverPid: devProcess.pid,
2010
+ serverUrl: `http://localhost:${port}`,
2011
+ logFile: outputFile,
2012
+ browserStatus: "launched",
2013
+ teachingMode: teachingMode
2014
+ }, null, 2)
2015
+ }
2016
+ ]
2017
+ };
2018
+ }
2019
+ catch (browserError) {
2020
+ // Browser launch failed, but server is running
2021
+ const errorMsg = browserError instanceof Error ? browserError.message : String(browserError);
2022
+ appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] [ERROR] Failed to launch browser: ${errorMsg}\n`);
2023
+ return {
2024
+ content: [
2025
+ {
2026
+ type: "text",
2027
+ text: JSON.stringify({
2028
+ status: "partial_success",
2029
+ message: `Server started successfully on http://localhost:${port}, but browser launch failed`,
2030
+ error: errorMsg,
2031
+ processId: processId,
2032
+ serverPid: devProcess.pid,
2033
+ serverUrl: `http://localhost:${port}`,
2034
+ logFile: outputFile,
2035
+ browserStatus: "failed",
2036
+ manualInstructions: "Open http://localhost:" + port + " in your browser manually"
2037
+ }, null, 2)
2038
+ }
2039
+ ]
2040
+ };
2041
+ }
2042
+ }
2043
+ catch (error) {
2044
+ throw new Error(`Failed to start frontend with browser: ${error instanceof Error ? error.message : String(error)}`);
2045
+ }
2046
+ }
1236
2047
  default:
1237
2048
  throw new Error(`Unknown tool: ${name}`);
1238
2049
  }