Skip to main content
Glama

autonomous-frontend-browser-tools

screenshot-service.ts13.7 kB
import fs from "fs"; import path from "path"; import os from "os"; import { exec } from "child_process"; import { fileURLToPath } from "url"; import { getScreenshotStoragePath, getDefaultDownloadsFolder } from "./modules/shared.js"; // Helper constants for ES module scope const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Unified Screenshot Service * * Provides a centralized system for organizing and saving screenshots * with intelligent project-based directory structure and URL categorization. */ export interface ScreenshotConfig { filename?: string; returnImageData?: boolean; projectName?: string; baseDirectory?: string; } export interface ScreenshotResult { filePath: string; filename: string; imageData?: string; projectDirectory: string; urlCategory: string; } export interface ScreenshotPathResolution { baseDirectory: string; projectDirectory: string; urlCategory: string; filename: string; fullPath: string; } export class ScreenshotService { private static instance: ScreenshotService; private constructor() {} // Project-level screenshot path is resolved centrally via modules/shared // using DEFAULT_SCREENSHOT_STORAGE_PATH from root projects.json when present. public static getInstance(): ScreenshotService { if (!ScreenshotService.instance) { ScreenshotService.instance = new ScreenshotService(); } return ScreenshotService.instance; } /** * Main method to save screenshots with unified organization */ public async saveScreenshot( base64Data: string, url?: string, config: ScreenshotConfig = {} ): Promise<ScreenshotResult> { // Clean base64 data const cleanBase64 = this.cleanBase64Data(base64Data); // Resolve the complete path structure const pathResolution = this.resolveScreenshotPath(url, config); // Ensure directory exists await this.ensureDirectoryExists(path.dirname(pathResolution.fullPath)); // Save the file await this.writeScreenshotFile(pathResolution.fullPath, cleanBase64); // Build result object const result: ScreenshotResult = { filePath: pathResolution.fullPath, filename: pathResolution.filename, projectDirectory: pathResolution.projectDirectory, urlCategory: pathResolution.urlCategory, }; // Include image data if requested if (config.returnImageData) { result.imageData = cleanBase64; } console.log(`Screenshot saved: ${pathResolution.fullPath}`); return result; } /** * Resolve the complete path structure for a screenshot */ private resolveScreenshotPath( url?: string, config: ScreenshotConfig = {} ): ScreenshotPathResolution { // 1. Determine base directory const baseDirectory = this.resolveBaseDirectory(config.baseDirectory); // 2. Determine project directory const projectDirectory = this.resolveProjectDirectory(config.projectName); // 3. Determine URL category (subfolder based on URL) const urlCategory = this.resolveUrlCategory(url); // 4. Generate filename const filename = this.generateFilename(url, config.filename); // 5. Build full path const fullPath = path.join( baseDirectory, projectDirectory, urlCategory, filename ); return { baseDirectory, projectDirectory, urlCategory, filename, fullPath, }; } /** * Determine the base directory for screenshots */ private resolveBaseDirectory(_configuredPath?: string): string { // Priority 1: DEFAULT_SCREENSHOT_STORAGE_PATH from projects.json (via shared) const projectPath = getScreenshotStoragePath(); if (projectPath && path.isAbsolute(projectPath)) { try { if (fs.existsSync(projectPath) || this.canCreateDirectory(projectPath)) { console.log("[info] Screenshot Service: Using configured DEFAULT_SCREENSHOT_STORAGE_PATH:", projectPath); return projectPath; } } catch (error) { console.warn(`Screenshot Service: Invalid configured screenshot path ${projectPath}:`, error); } } // Priority 2: Default downloads folder from shared helper const downloadsBase = getDefaultDownloadsFolder(); try { if (!fs.existsSync(downloadsBase)) { fs.mkdirSync(downloadsBase, { recursive: true }); } } catch (error) { console.warn("Screenshot Service: Failed to ensure downloads folder:", downloadsBase, error); } console.log("[info] Screenshot Service: Using default downloads folder:", downloadsBase); return downloadsBase; } /** * Determine project directory name */ private resolveProjectDirectory(configuredProject?: string): string { console.log("Screenshot Service: Resolving project directory"); // First priority: configuredProject parameter (from MCP server) if (configuredProject && configuredProject.trim()) { console.log( "Screenshot Service: Using configured project name:", configuredProject ); return this.sanitizeDirectoryName(configuredProject); } // Fallback to generic folder console.log( "Screenshot Service: Using fallback project name: default-project" ); return "default-project"; } /** * Determine URL category (subfolder based on URL path) */ private resolveUrlCategory(url?: string): string { if (!url || url === "about:blank") { return "general"; } try { const urlObj = new URL(url); // Handle localhost with specific logic if (urlObj.hostname === "localhost" || urlObj.hostname === "127.0.0.1") { return this.categorizeLocalUrl(urlObj); } // Handle staging/production environments if ( urlObj.hostname.includes("staging") || urlObj.hostname.includes("dev") ) { return this.categorizeEnvironmentUrl(urlObj, "staging"); } if ( urlObj.hostname.includes("prod") || !urlObj.hostname.includes("localhost") ) { return this.categorizeEnvironmentUrl(urlObj, "production"); } // Default path-based categorization return this.categorizeByPath(urlObj.pathname); } catch (error) { console.warn(`Failed to parse URL for categorization: ${url}`, error); return "uncategorized"; } } /** * Categorize localhost URLs */ private categorizeLocalUrl(urlObj: URL): string { const pathSegments = urlObj.pathname .split("/") .filter((segment) => segment.length > 0); if (pathSegments.length === 0) { return "home"; } // Use first meaningful path segment const category = pathSegments[0]; return this.sanitizeDirectoryName(category); } /** * Categorize URLs by environment */ private categorizeEnvironmentUrl(urlObj: URL, environment: string): string { const pathCategory = this.categorizeByPath(urlObj.pathname); return `${environment}/${pathCategory}`; } /** * Categorize by URL path */ private categorizeByPath(pathname: string): string { const pathSegments = pathname .split("/") .filter((segment) => segment.length > 0); if (pathSegments.length === 0) { return "home"; } // Use first meaningful path segment return this.sanitizeDirectoryName(pathSegments[0]); } /** * Generate screenshot filename */ private generateFilename(url?: string, customFilename?: string): string { const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); if (customFilename && customFilename.trim()) { const sanitized = this.sanitizeFilename(customFilename); return `${timestamp}_${sanitized}.png`; } // Generate filename from URL if (url) { const urlBasedName = this.generateUrlBasedFilename(url); return `${timestamp}_${urlBasedName}.png`; } // Fallback to timestamp only return `${timestamp}_screenshot.png`; } /** * Generate filename based on URL content */ private generateUrlBasedFilename(url: string): string { try { const urlObj = new URL(url); // Extract meaningful parts from the URL const pathSegments = urlObj.pathname .split("/") .filter((segment) => segment.length > 0); if (pathSegments.length === 0) { return "homepage"; } // Use last meaningful segment or combine multiple segments if (pathSegments.length === 1) { return this.sanitizeFilename(pathSegments[0]); } // Combine last 2 segments for more context const lastTwoSegments = pathSegments.slice(-2).join("-"); return this.sanitizeFilename(lastTwoSegments); } catch (error) { return "page"; } } /** * Detect git repository name */ private detectGitProjectName(): string | null { try { // Try to get git remote origin URL const { execSync } = require("child_process"); const remoteUrl = execSync("git config --get remote.origin.url", { encoding: "utf8", cwd: process.cwd(), timeout: 1000, }).trim(); // Extract repository name from git URL const match = remoteUrl.match(/\/([^\/]+?)(?:\.git)?$/); if (match && match[1]) { return match[1]; } } catch (error) { // Git not available or not in a git repository } return null; } /** * Clean base64 data by removing data URL prefix */ private cleanBase64Data(base64Data: string): string { return base64Data.replace(/^data:image\/[^;]+;base64,/, ""); } /** * Ensure directory exists */ private async ensureDirectoryExists(dirPath: string): Promise<void> { try { await fs.promises.mkdir(dirPath, { recursive: true }); } catch (error) { throw new Error(`Failed to create directory ${dirPath}: ${error}`); } } /** * Write screenshot file to disk */ private async writeScreenshotFile( filePath: string, base64Data: string ): Promise<void> { try { await fs.promises.writeFile(filePath, base64Data, "base64"); } catch (error) { throw new Error(`Failed to write screenshot file ${filePath}: ${error}`); } } /** * Check if a directory can be created (either doesn't exist but parent exists, or already exists) */ private canCreateDirectory(dirPath: string): boolean { try { // If directory already exists, return true if (fs.existsSync(dirPath)) { return fs.statSync(dirPath).isDirectory(); } // Check if parent directory exists const parentDir = path.dirname(dirPath); return fs.existsSync(parentDir) && fs.statSync(parentDir).isDirectory(); } catch (error) { return false; } } /** * Get default downloads folder based on OS */ // Default downloads folder is provided by modules/shared helper /** * Sanitize directory name for filesystem */ private sanitizeDirectoryName(name: string): string { return name .replace(/[\/\\?%*:|"<>\s#&+=]/g, "-") // Replace invalid chars with dash .replace(/-+/g, "-") // Replace multiple dashes with single dash .replace(/^-+|-+$/g, "") // Remove leading/trailing dashes .toLowerCase() // Convert to lowercase for consistency .substring(0, 50); // Limit length } /** * Sanitize filename for filesystem */ private sanitizeFilename(name: string): string { return name .replace(/[\/\\?%*:|"<>\s#&+=]/g, "_") // Replace invalid chars with underscore .replace(/_+/g, "_") // Replace multiple underscores with single underscore .replace(/^_+|_+$/g, "") // Remove leading/trailing underscores .substring(0, 100); // Limit length } /** * Execute auto-paste functionality on macOS */ public async executeAutoPaste(filePath: string): Promise<void> { if (os.platform() !== "darwin") { console.log("Auto-paste is only supported on macOS"); return; } const appleScript = ` set imagePath to "${filePath}" try set the clipboard to (read (POSIX file imagePath) as «class PNGf») on error errMsg log "Error copying image to clipboard: " & errMsg return "Failed to copy image to clipboard: " & errMsg end try try tell application "Cursor" activate end tell on error errMsg log "Error activating Cursor: " & errMsg return "Failed to activate Cursor: " & errMsg end try delay 3 try tell application "System Events" tell process "Cursor" if (count of windows) is 0 then return "No windows found in Cursor" end if keystroke "v" using command down delay 1 keystroke "here is the screenshot" delay 1 key code 36 delay 0.5 keystroke return return "Successfully pasted screenshot into Cursor" end tell end tell on error errMsg log "Error in System Events: " & errMsg return "Failed in System Events: " & errMsg end try `; return new Promise((resolve, reject) => { exec(`osascript -e '${appleScript}'`, (error, stdout, stderr) => { if (error) { console.error(`Auto-paste error: ${error.message}`); console.error(`stderr: ${stderr}`); reject(error); } else { console.log(`Auto-paste executed successfully: ${stdout}`); resolve(); } }); }); } } export default ScreenshotService;

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