Skip to main content
Glama
puppeteer-controller.ts9.65 kB
/** * PinePaper Puppeteer Controller * * Controls a real browser instance running PinePaper Studio. * Executes code directly via page.evaluate() and provides screenshots. */ import type { Browser, Page } from 'puppeteer'; // ============================================================================= // TYPES // ============================================================================= export interface BrowserControllerConfig { /** PinePaper Studio URL (default: https://pinepaper.studio) */ studioUrl?: string; /** Whether to run browser in headless mode (default: false for visibility) */ headless?: boolean; /** Browser viewport width (default: 1280) */ viewportWidth?: number; /** Browser viewport height (default: 800) */ viewportHeight?: number; /** Timeout for page operations in ms (default: 30000) */ timeout?: number; } export interface ExecuteResult { success: boolean; result?: unknown; error?: string; screenshot?: string; // base64 encoded } // ============================================================================= // BROWSER CONTROLLER CLASS // ============================================================================= /** * Controls PinePaper Studio via Puppeteer. * * This controller: * 1. Launches a browser and navigates to PinePaper Studio * 2. Executes JavaScript code directly in the page context * 3. Captures screenshots to show results */ export class PinePaperBrowserController { private browser: Browser | null = null; private page: Page | null = null; private config: Required<BrowserControllerConfig>; private isConnected = false; constructor(config: BrowserControllerConfig = {}) { // Ensure URL points to /editor where the code console and API are available let studioUrl = config.studioUrl || 'https://pinepaper.studio'; if (!studioUrl.includes('/editor')) { studioUrl = studioUrl.replace(/\/$/, '') + '/editor'; } this.config = { studioUrl, headless: config.headless ?? false, // Default to visible browser viewportWidth: config.viewportWidth || 1280, viewportHeight: config.viewportHeight || 800, timeout: config.timeout || 30000, }; } /** * Check if connected to browser */ get connected(): boolean { return this.isConnected && this.page !== null; } /** * Get current studio URL */ get studioUrl(): string { return this.config.studioUrl; } /** * Launch browser and navigate to PinePaper Studio */ async connect(): Promise<void> { if (this.isConnected) { console.error('[PinePaper] Already connected'); return; } try { // Dynamic import to avoid issues when puppeteer isn't installed const puppeteer = await import('puppeteer'); console.error('[PinePaper] Launching browser...'); this.browser = await puppeteer.default.launch({ headless: this.config.headless, args: [ '--no-sandbox', '--disable-setuid-sandbox', `--window-size=${this.config.viewportWidth},${this.config.viewportHeight}`, ], }); this.page = await this.browser.newPage(); await this.page.setViewport({ width: this.config.viewportWidth, height: this.config.viewportHeight, }); console.error(`[PinePaper] Navigating to ${this.config.studioUrl}...`); await this.page.goto(this.config.studioUrl, { waitUntil: 'networkidle2', timeout: this.config.timeout, }); // Wait for PinePaper to be ready (check for app object) console.error('[PinePaper] Waiting for PinePaper Studio to initialize...'); await this.page.waitForFunction( () => { // Check if PinePaper's global app or API is available return ( typeof (window as any).app !== 'undefined' || typeof (window as any).pinepaper !== 'undefined' || typeof (window as any).paper !== 'undefined' ); }, { timeout: this.config.timeout } ); this.isConnected = true; console.error('[PinePaper] Connected to PinePaper Studio'); } catch (error) { await this.disconnect(); throw new Error( `Failed to connect to PinePaper Studio: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } /** * Disconnect and close browser */ async disconnect(): Promise<void> { if (this.browser) { try { await this.browser.close(); } catch { // Ignore errors during cleanup } } this.browser = null; this.page = null; this.isConnected = false; console.error('[PinePaper] Disconnected from browser'); } /** * Execute JavaScript code in PinePaper Studio */ async executeCode(code: string, takeScreenshot = true): Promise<ExecuteResult> { if (!this.page || !this.isConnected) { return { success: false, error: 'Not connected to PinePaper Studio. Call connect() first.', }; } try { // Execute the code in page context const result = await this.page.evaluate((codeToRun: string) => { try { // eslint-disable-next-line no-eval const evalResult = eval(codeToRun); // Handle promises if (evalResult instanceof Promise) { return evalResult.then((r) => ({ success: true, result: r })); } return { success: true, result: evalResult }; } catch (e) { return { success: false, error: e instanceof Error ? e.message : 'Execution error', }; } }, code); // Take screenshot if requested let screenshot: string | undefined; if (takeScreenshot && result.success) { screenshot = await this.takeScreenshot(); } return { ...result, screenshot, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown execution error', }; } } /** * Take a screenshot of the current canvas */ async takeScreenshot(): Promise<string | undefined> { if (!this.page) { return undefined; } try { // Try to capture just the canvas element, fallback to full page const canvasElement = await this.page.$('canvas'); if (canvasElement) { const screenshot = await canvasElement.screenshot({ encoding: 'base64', type: 'png', }); return screenshot as string; } // Fallback to full page const screenshot = await this.page.screenshot({ encoding: 'base64', type: 'png', fullPage: false, }); return screenshot as string; } catch { return undefined; } } /** * Get the current page URL */ async getCurrentUrl(): Promise<string | undefined> { return this.page?.url(); } /** * Navigate to a different URL */ async navigateTo(url: string): Promise<void> { if (!this.page) { throw new Error('Not connected to browser'); } await this.page.goto(url, { waitUntil: 'networkidle2', timeout: this.config.timeout, }); } /** * Reload the page */ async reload(): Promise<void> { if (!this.page) { throw new Error('Not connected to browser'); } await this.page.reload({ waitUntil: 'networkidle2', timeout: this.config.timeout, }); } /** * Refresh the page and wait for PinePaper to be ready again. * This is the most reliable way to get a clean canvas. */ async refreshPage(): Promise<void> { if (!this.page) { throw new Error('Not connected to browser'); } console.error('[PinePaper] Refreshing page...'); await this.page.reload({ waitUntil: 'networkidle2', timeout: this.config.timeout, }); // Wait for PinePaper to be ready again console.error('[PinePaper] Waiting for PinePaper Studio to reinitialize...'); await this.page.waitForFunction( () => { return ( typeof (window as any).app !== 'undefined' || typeof (window as any).pinepaper !== 'undefined' || typeof (window as any).paper !== 'undefined' ); }, { timeout: this.config.timeout } ); console.error('[PinePaper] Page refreshed and ready'); } /** * Check if PinePaper API is available */ async checkPinePaperReady(): Promise<boolean> { if (!this.page) { return false; } try { return await this.page.evaluate(() => { return ( typeof (window as any).app !== 'undefined' || typeof (window as any).pinepaper !== 'undefined' || typeof (window as any).paper !== 'undefined' ); }); } catch { return false; } } } // ============================================================================= // SINGLETON INSTANCE // ============================================================================= let globalController: PinePaperBrowserController | null = null; /** * Get or create the global browser controller instance */ export function getBrowserController( config?: BrowserControllerConfig ): PinePaperBrowserController { if (!globalController) { globalController = new PinePaperBrowserController(config); } return globalController; } /** * Reset the global controller (for testing) */ export function resetBrowserController(): void { if (globalController) { globalController.disconnect().catch(() => {}); globalController = null; } }

Latest Blog Posts

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/pinepaper/mcp-server'

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