Skip to main content
Glama

autonomous-frontend-browser-tools

shared.js15.6 kB
/** * Refactor Temp: Top Scaffolding — env/config, connection bootstrap, shared utils * This module aggregates non-tool-specific code extracted from browser-connector.ts. * Exports are named to allow fine-grained re-use during the transition. * * NOTE: This file holds only stateless/shared utilities and configuration helpers. * Express bindings and WebSocket state remain in browser-connector.ts. */ import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import os from "os"; // Recreate ESM helper constants and export them for passthrough usage export const __filename = fileURLToPath(import.meta.url); export const __dirname = path.dirname(__filename); /** * Returns the default downloads folder path for the current OS, used for screenshot storage fallback. */ export function getDefaultDownloadsFolder() { const homeDir = os.homedir(); // Downloads folder is typically the same path on Windows, macOS, and Linux const downloadsPath = path.join(homeDir, "Downloads", "mcp-screenshots"); return downloadsPath; } /** * Helper to recursively truncate strings in any data structure * (moved from browser-connector.ts) */ export function truncateStringsInData(data, maxLength) { if (typeof data === "string") { return data.length > maxLength ? data.substring(0, maxLength) + "... (truncated)" : data; } if (Array.isArray(data)) { return data.map((item) => truncateStringsInData(item, maxLength)); } if (typeof data === "object" && data !== null) { const result = {}; for (const [key, value] of Object.entries(data)) { result[key] = truncateStringsInData(value, maxLength); } return result; } return data; } /** * Helper to safely parse and process JSON strings * (moved from browser-connector.ts) */ export function processJsonString(jsonString, maxLength) { try { // Try to parse the string as JSON const parsed = JSON.parse(jsonString); // Process any strings within the parsed JSON const processed = truncateStringsInData(parsed, maxLength); // Stringify the processed data return JSON.stringify(processed); } catch { // If it's not valid JSON, treat it as a regular string return truncateStringsInData(jsonString, maxLength); } } /** * Helper to calculate size of a log entry * (moved from browser-connector.ts) */ export function calculateLogSize(log) { return JSON.stringify(log).length; } /** * Helper to process logs based on current settings * (moved from browser-connector.ts) */ export function processLogsWithSettings(logs, currentSettings) { return logs.map((log) => { const processedLog = { ...log }; if (log.type === "network-request") { // Handle headers visibility if (!currentSettings.showRequestHeaders) { delete processedLog.requestHeaders; } if (!currentSettings.showResponseHeaders) { delete processedLog.responseHeaders; } } return processedLog; }); } /** Maximum in-memory cache size for detailed network logs */ export const MAX_DETAILED_NETWORK_LOG_CACHE = 50; /** * Keep a backward-compatible signature: if settings are omitted, defaults are used. */ export function truncateLogsToQueryLimit(logs, currentSettings) { // Overload support: original single-arg signature (logs only) // and new two-arg signature (logs, settings). If settings omitted, // return logs unchanged to preserve original behavior at call sites // that expect default processing in the caller. if (logs.length === 0) return logs; const effectiveSettings = currentSettings ?? { queryLimit: 30000, showRequestHeaders: false, showResponseHeaders: false, }; // First process logs according to current settings const processedLogs = processLogsWithSettings(logs, effectiveSettings); let currentSize = 0; const result = []; for (const log of processedLogs) { const logSize = calculateLogSize(log); // Check if adding this log would exceed the limit if (currentSize + logSize > effectiveSettings.queryLimit) { console.log(`Reached query limit (${currentSize}/${effectiveSettings.queryLimit}), truncating logs`); break; } // Add log and update size result.push(log); currentSize += logSize; console.log(`Added log of size ${logSize}, total size now: ${currentSize}`); } return result; } /** * Converts a file path to the appropriate format for the current platform * Handles Windows, WSL, macOS and Linux path formats * (moved from browser-connector.ts) */ export function convertPathForCurrentPlatform(inputPath) { const platform = os.platform(); // If no path provided, return as is if (!inputPath) return inputPath; console.log(`Converting path "${inputPath}" for platform: ${platform}`); // Windows-specific conversion if (platform === "win32") { // Convert forward slashes to backslashes return inputPath.replace(/\//g, "\\"); } // Linux/Mac-specific conversion if (platform === "linux" || platform === "darwin") { // Check if this is a Windows UNC path (starts with \\) if (inputPath.startsWith("\\\\") || inputPath.includes("\\")) { // Check if this is a WSL path (contains wsl.localhost or wsl$) if (inputPath.includes("wsl.localhost") || inputPath.includes("wsl$")) { // Extract the path after the distribution name // Handle both \\wsl.localhost\Ubuntu\path and \\wsl$\Ubuntu\path formats const parts = inputPath.split("\\").filter((part) => part.length > 0); console.log("Path parts:", parts); // Find the index after the distribution name const distNames = [ "Ubuntu", "Debian", "kali", "openSUSE", "SLES", "Fedora", ]; // Find the distribution name in the path let distIndex = -1; for (const dist of distNames) { const index = parts.findIndex((part) => part === dist || part.toLowerCase() === dist.toLowerCase()); if (index !== -1) { distIndex = index; break; } } if (distIndex !== -1 && distIndex + 1 < parts.length) { // Reconstruct the path as a native Linux path const linuxPath = "/" + parts.slice(distIndex + 1).join("/"); console.log(`Converted Windows WSL path "${inputPath}" to Linux path "${linuxPath}"`); return linuxPath; } // If we couldn't find a distribution name but it's clearly a WSL path, // try to extract everything after wsl.localhost or wsl$ const wslIndex = parts.findIndex((part) => part === "wsl.localhost" || part === "wsl$" || part.toLowerCase() === "wsl.localhost" || part.toLowerCase() === "wsl$"); if (wslIndex !== -1 && wslIndex + 2 < parts.length) { // Skip the WSL prefix and distribution name const linuxPath = "/" + parts.slice(wslIndex + 2).join("/"); console.log(`Converted Windows WSL path "${inputPath}" to Linux path "${linuxPath}"`); return linuxPath; } } // For non-WSL Windows paths, just normalize the slashes const normalizedPath = inputPath .replace(/\\\\/g, "/") .replace(/\\/g, "/"); console.log(`Converted Windows UNC path "${inputPath}" to "${normalizedPath}"`); return normalizedPath; } // Handle Windows drive letters (e.g., C:\path\to\file) if (/^[A-Z]:\\/i.test(inputPath)) { // Convert Windows drive path to Linux/Mac compatible path const normalizedPath = inputPath .replace(/^[A-Z]:\\/i, "/") .replace(/\\/g, "/"); console.log(`Converted Windows drive path "${inputPath}" to "${normalizedPath}"`); return normalizedPath; } } // Return the original path if no conversion was needed or possible return inputPath; } // Load project configuration // Lightweight in-process cache to avoid repeated filesystem reads and duplicate logs let cachedProjectsConfig = null; let cachedProjectsConfigPath = null; let cachedProjectsConfigMtimeMs = null; let hasLoggedProjectsConfig = false; /** * Resolve the path to projects.json with priority: * 1) env AFBT_PROJECTS_JSON (absolute path) * 2) CWD/chrome-extension/projects.json (Setup UI writes here) * 3) Packaged fallback relative to this module (node_modules/...) */ function resolveProjectsJsonPath() { const candidates = []; if (process.env.AFBT_PROJECTS_JSON) { candidates.push(path.resolve(process.env.AFBT_PROJECTS_JSON)); } // 1) Current working directory (works when server is started from repo root) candidates.push(path.join(process.cwd(), "projects.json")); // 2) Parent of CWD (works when server CWD is browser-tools-server/) try { candidates.push(path.resolve(process.cwd(), "..", "projects.json")); } catch { } // 3) Module-relative repo root (dev/local builds) try { candidates.push(path.resolve(__dirname, "..", "..", "projects.json")); } catch { } try { const home = os.homedir(); if (home) candidates.push(path.resolve(home, ".afbt", "projects.json")); } catch { } const chosen = candidates.find((p) => fs.existsSync(p)); if (!chosen) { throw new Error(`projects.json not found. Checked: ${candidates.join(", ")}. Set AFBT_PROJECTS_JSON or use the Setup UI.`); } return chosen; } export function loadProjectConfig() { try { const configPath = resolveProjectsJsonPath(); // Only log once for discoverability (shows chosen path) if (!hasLoggedProjectsConfig) { console.log(`[INFO] Browser Connector: Loading projects.json from ${configPath}`); } if (!fs.existsSync(configPath)) { if (!hasLoggedProjectsConfig) { console.log(`[WARN] Browser Connector: projects.json not found at: ${configPath}`); } cachedProjectsConfig = null; cachedProjectsConfigPath = null; cachedProjectsConfigMtimeMs = null; hasLoggedProjectsConfig = true; return null; } // Reload when the file changes or path differs let stat = null; try { stat = fs.statSync(configPath); } catch { stat = null; } const mtimeMs = stat ? stat.mtimeMs : null; const shouldReload = !cachedProjectsConfig || cachedProjectsConfigPath !== configPath || (mtimeMs !== null && cachedProjectsConfigMtimeMs !== mtimeMs); if (shouldReload) { const configData = fs.readFileSync(configPath, "utf8"); const parsed = JSON.parse(configData); cachedProjectsConfig = parsed; const previousPath = cachedProjectsConfigPath; cachedProjectsConfigPath = configPath; cachedProjectsConfigMtimeMs = mtimeMs; if (previousPath && previousPath !== configPath) { console.log(`[INFO] Browser Connector: Reloaded projects.json from ${configPath} (was ${previousPath})`); } if (!hasLoggedProjectsConfig) { const projectCount = Object.keys(parsed.projects || {}).length; console.log(`[INFO] Browser Connector: Loaded projects.json (projects=${projectCount}, defaultProject=${parsed.defaultProject})`); } } hasLoggedProjectsConfig = true; return cachedProjectsConfig; } catch (error) { console.error("Browser Connector: Error loading projects config:", error); return null; } } // Get configuration value with fallback priority: // 1. Environment variable (highest priority) // 2. Project config file // 3. Default value (lowest priority) export function getConfigValue(key, defaultValue) { // First check environment variables if (process.env[key]) { return process.env[key]; } // Security: never read embedding API keys from projects.json if (key === "OPENAI_API_KEY" || key === "GEMINI_API_KEY") { return defaultValue; } // Then check project config const projectsConfig = loadProjectConfig(); if (projectsConfig) { const activeProject = process.env.ACTIVE_PROJECT || projectsConfig.defaultProject; const project = projectsConfig.projects[activeProject]; if (project && project.config[key]) { return project.config[key]; } } // Finally return default value return defaultValue; } // Get screenshot storage path from project config export function getScreenshotStoragePath() { const projectsConfig = loadProjectConfig(); if (projectsConfig && projectsConfig.DEFAULT_SCREENSHOT_STORAGE_PATH) { return projectsConfig.DEFAULT_SCREENSHOT_STORAGE_PATH; } return undefined; } // Get active project name export function getActiveProjectName() { const projectsConfig = loadProjectConfig(); if (projectsConfig) { const active = process.env.ACTIVE_PROJECT || projectsConfig.defaultProject; return active; } return undefined; } // ==== Port discovery and server utilities ==== /** * Check if a port is available */ export function isPortAvailable(port) { return new Promise((resolve) => { // Use dynamic import for ESM compatibility instead of require() import("net") .then((netMod) => { const server = netMod.createServer(); server.listen(port, () => { server.once("close", () => { resolve(true); }); server.close(); }); server.on("error", () => { resolve(false); }); }) .catch(() => { // If net import somehow fails, mark port as unavailable resolve(false); }); }); } /** * Find an available port starting from the requested port */ export async function getAvailablePort(requestedPort) { const maxAttempts = 10; for (let i = 0; i < maxAttempts; i++) { const portToTry = requestedPort + i; const available = await isPortAvailable(portToTry); if (available) { return portToTry; } } throw new Error(`No available port found in range ${requestedPort}-${requestedPort + maxAttempts - 1}`); } // ==== Log management ==== // Global log storage (moved from browser-connector.ts) export const logs = []; export const networkLogs = []; export const detailedNetworkLogCache = []; /** * Clear all logs */ export function clearAllLogs() { logs.length = 0; networkLogs.length = 0; detailedNetworkLogCache.length = 0; console.log("All logs cleared"); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Winds-AI/Frontend-development-MCP-tools-public'

If you have feedback or need assistance with the MCP directory API, please join our Discord server