UNPKG

claude-playwright

Version:

Seamless integration between Claude Code and Playwright MCP for efficient browser automation and testing

460 lines 18.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SessionManager = void 0; const fs_extra_1 = __importDefault(require("fs-extra")); const path_1 = __importDefault(require("path")); const chalk_1 = __importDefault(require("chalk")); class SessionManager { constructor(workingDir) { this.sessionsDir = './playwright-sessions'; this.profilesDir = './browser-profiles'; if (workingDir) { this.sessionsDir = path_1.default.join(workingDir, 'playwright-sessions'); this.profilesDir = path_1.default.join(workingDir, 'browser-profiles'); } this.configPath = path_1.default.join(this.sessionsDir, '.config.json'); } /** * Get session configuration */ async getSessionConfig() { try { if (await fs_extra_1.default.pathExists(this.configPath)) { return await fs_extra_1.default.readJSON(this.configPath); } } catch (error) { console.warn(chalk_1.default.yellow('Warning: Could not read session config')); } // Return default config return { defaultSessionTimeout: 28800000, // 8 hours autoCleanupExpired: true, autoLoadLatest: true, maxConcurrentSessions: 5, sessionBackupEnabled: true, profileIntegration: { enabled: true, autoSelectProfile: true, fallbackProfile: 'default' }, logging: { enabled: true, level: 'info' } }; } /** * Save a browser session with 8-hour expiry */ async saveSession(name, storageState, options) { await fs_extra_1.default.ensureDir(this.sessionsDir); const sessionPath = path_1.default.join(this.sessionsDir, `${name}.json`); const sessionData = { name, createdAt: Date.now(), storageState, expiresAt: Date.now() + (8 * 60 * 60 * 1000), // 8 hours browserProfile: options?.browserProfile, metadata: options?.metadata }; await fs_extra_1.default.writeJSON(sessionPath, sessionData, { spaces: 2 }); console.log(chalk_1.default.green('✓') + ` Session saved: ${name}`); return sessionPath; } /** * Load a stored session and check expiry */ async loadSession(name) { const sessionPath = path_1.default.join(this.sessionsDir, `${name}.json`); if (!await fs_extra_1.default.pathExists(sessionPath)) { throw new Error(`Session not found: ${name}`); } const session = await fs_extra_1.default.readJSON(sessionPath); // Check if expired if (Date.now() > session.expiresAt) { throw new Error(`Session expired: ${name}. Session was valid until ${new Date(session.expiresAt).toLocaleString()}`); } return session.storageState; } /** * Get full session data including metadata */ async getSessionData(name) { const sessionPath = path_1.default.join(this.sessionsDir, `${name}.json`); if (!await fs_extra_1.default.pathExists(sessionPath)) { throw new Error(`Session not found: ${name}`); } const session = await fs_extra_1.default.readJSON(sessionPath); // Check if expired if (Date.now() > session.expiresAt) { throw new Error(`Session expired: ${name}. Session was valid until ${new Date(session.expiresAt).toLocaleString()}`); } return session; } /** * List all sessions with their status */ async listSessions() { await fs_extra_1.default.ensureDir(this.sessionsDir); const files = await fs_extra_1.default.readdir(this.sessionsDir); const sessions = []; for (const file of files) { if (file.endsWith('.json')) { try { const session = await fs_extra_1.default.readJSON(path_1.default.join(this.sessionsDir, file)); // Add null checks and default values const safeName = session.name || 'unknown'; const safeCreatedAt = session.createdAt || Date.now(); const safeExpiresAt = session.expiresAt || Date.now(); sessions.push({ name: safeName, created: new Date(safeCreatedAt).toLocaleString(), expires: new Date(safeExpiresAt).toLocaleString(), expired: Date.now() > safeExpiresAt, browserProfile: session.browserProfile }); } catch (error) { console.warn(chalk_1.default.yellow(`Warning: Could not read session file ${file}`)); } } } // Sort by creation date (newest first) return sessions.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime()); } /** * Clear expired sessions */ async clearExpiredSessions() { await fs_extra_1.default.ensureDir(this.sessionsDir); const files = await fs_extra_1.default.readdir(this.sessionsDir); let clearedCount = 0; for (const file of files) { if (file.endsWith('.json')) { try { const sessionPath = path_1.default.join(this.sessionsDir, file); const session = await fs_extra_1.default.readJSON(sessionPath); if (Date.now() > session.expiresAt) { await fs_extra_1.default.remove(sessionPath); clearedCount++; console.log(chalk_1.default.gray(`Removed expired session: ${session.name}`)); } } catch (error) { console.warn(chalk_1.default.yellow(`Warning: Could not process session file ${file}`)); } } } return clearedCount; } /** * Delete a specific session */ async deleteSession(name) { const sessionPath = path_1.default.join(this.sessionsDir, `${name}.json`); if (!await fs_extra_1.default.pathExists(sessionPath)) { return false; } await fs_extra_1.default.remove(sessionPath); console.log(chalk_1.default.green('✓') + ` Session deleted: ${name}`); return true; } /** * Check if session exists and is valid */ async isSessionValid(name) { try { const sessionPath = path_1.default.join(this.sessionsDir, `${name}.json`); if (!await fs_extra_1.default.pathExists(sessionPath)) { return false; } const session = await fs_extra_1.default.readJSON(sessionPath); return Date.now() <= session.expiresAt; } catch (error) { return false; } } /** * Update session expiry (extend for another 8 hours) */ async extendSession(name) { try { const sessionPath = path_1.default.join(this.sessionsDir, `${name}.json`); if (!await fs_extra_1.default.pathExists(sessionPath)) { throw new Error(`Session not found: ${name}`); } const session = await fs_extra_1.default.readJSON(sessionPath); session.expiresAt = Date.now() + (8 * 60 * 60 * 1000); // Extend for 8 more hours await fs_extra_1.default.writeJSON(sessionPath, session, { spaces: 2 }); console.log(chalk_1.default.green('✓') + ` Session extended: ${name} (expires: ${new Date(session.expiresAt).toLocaleString()})`); return true; } catch (error) { console.error(chalk_1.default.red('Failed to extend session:'), error.message); return false; } } /** * Auto-extend session if it expires in less than 2 hours */ async autoExtendSession(name) { try { const sessionData = await this.getSessionData(name); const expiresAt = sessionData.expiresAt || 0; const now = Date.now(); const hoursRemaining = (expiresAt - now) / (1000 * 60 * 60); // Auto-extend if less than 2 hours remain and more than 0 hours if (hoursRemaining < 2 && hoursRemaining > 0) { console.log(chalk_1.default.yellow(`⏰ Session "${name}" expires in ${hoursRemaining.toFixed(1)} hours, auto-extending...`)); return await this.extendSession(name); } // Session is still valid for more than 2 hours if (hoursRemaining >= 2) { console.log(chalk_1.default.gray(`✓ Session "${name}" is valid for ${hoursRemaining.toFixed(1)} more hours`)); return true; } // Session is already expired if (hoursRemaining <= 0) { console.log(chalk_1.default.red(`❌ Session "${name}" has already expired`)); return false; } return true; } catch (error) { // Session might not exist or be invalid, that's ok for auto-extend console.log(chalk_1.default.gray(`ℹ️ Could not auto-extend session "${name}": ${error.message}`)); return false; } } /** * Check session health and provide recommendations */ async checkSessionHealth(name) { try { const sessionData = await this.getSessionData(name); const expiresAt = sessionData.expiresAt || 0; const now = Date.now(); const hoursRemaining = (expiresAt - now) / (1000 * 60 * 60); let recommendation = ''; let needsExtension = false; if (hoursRemaining <= 0) { recommendation = 'Session has expired. Create a new session.'; } else if (hoursRemaining < 1) { recommendation = 'Session expires very soon. Extend or recreate immediately.'; needsExtension = true; } else if (hoursRemaining < 2) { recommendation = 'Session will be auto-extended on next use.'; needsExtension = true; } else if (hoursRemaining < 4) { recommendation = 'Session is valid but will need extension soon.'; } else { recommendation = 'Session is healthy and valid.'; } return { isValid: hoursRemaining > 0, hoursRemaining, recommendation, needsExtension }; } catch (error) { return { isValid: false, hoursRemaining: 0, recommendation: `Session not found or invalid: ${error.message}`, needsExtension: false }; } } /** * Batch check health of all sessions */ async checkAllSessionsHealth() { const sessions = await this.listSessions(); const healthChecks = []; for (const session of sessions) { const health = await this.checkSessionHealth(session.name); healthChecks.push({ name: session.name, ...health }); } return healthChecks; } /** * Find the latest valid session for auto-loading (MCP integration) */ async findLatestValidSession() { try { const sessions = await this.listSessions(); const validSessions = sessions.filter(s => !s.expired); if (validSessions.length === 0) { return null; } // Get the most recent valid session const latestSessionName = validSessions[0].name; return await this.getSessionData(latestSessionName); } catch (error) { console.warn(chalk_1.default.yellow('Warning: Could not find latest valid session'), error.message); return null; } } /** * Get the currently active session name from active-session.json */ async getActiveSession() { try { const activeSessionConfig = path_1.default.join(this.sessionsDir, 'active-session.json'); if (!await fs_extra_1.default.pathExists(activeSessionConfig)) { return null; } const config = await fs_extra_1.default.readJson(activeSessionConfig); return config.activeSession || null; } catch (error) { return null; } } /** * Switch to a different session by updating active-session.json */ async switchSession(sessionName) { try { // Check if session exists const sessionFile = path_1.default.join(this.sessionsDir, `${sessionName}.json`); if (!await fs_extra_1.default.pathExists(sessionFile)) { console.error(chalk_1.default.red(`Session not found: ${sessionName}`)); return false; } // Update or create active-session.json const activeSessionConfig = path_1.default.join(this.sessionsDir, 'active-session.json'); let config = { activeSession: sessionName, switchedAt: new Date().toISOString(), availableSessions: [] }; // Preserve existing config if it exists if (await fs_extra_1.default.pathExists(activeSessionConfig)) { try { const existingConfig = await fs_extra_1.default.readJson(activeSessionConfig); config.availableSessions = existingConfig.availableSessions || []; } catch (err) { // Use default config } } await fs_extra_1.default.writeJson(activeSessionConfig, config, { spaces: 2 }); return true; } catch (error) { console.error(chalk_1.default.red('Failed to switch session:'), error.message); return false; } } /** * Get MCP-compatible session data with environment variables */ async getMCPSessionData(sessionName) { try { let session = null; if (sessionName) { session = await this.getSessionData(sessionName); } else { session = await this.findLatestValidSession(); } if (!session) { return { hasSession: false, sessionName: null, storageState: null, browserProfile: null, metadata: null }; } return { hasSession: true, sessionName: session.name, storageState: session.storageState, browserProfile: session.browserProfile, metadata: session.metadata, expiresAt: session.expiresAt, createdAt: session.createdAt }; } catch (error) { console.warn(chalk_1.default.yellow('Warning: Could not get MCP session data'), error.message); return { hasSession: false, sessionName: null, storageState: null, browserProfile: null, metadata: null }; } } /** * Auto-save session during MCP usage */ async autoSaveSession(storageState, metadata) { try { const config = await this.getSessionConfig(); if (!config.sessionBackupEnabled) { return null; } const sessionName = `auto-backup-${Date.now()}`; await this.saveSession(sessionName, storageState, { metadata }); // Clean up old auto-backup sessions to prevent clutter await this.cleanupOldAutoBackups(config.maxConcurrentSessions || 5); return sessionName; } catch (error) { console.warn(chalk_1.default.yellow('Warning: Auto-save failed'), error.message); return null; } } /** * Clean up old auto-backup sessions */ async cleanupOldAutoBackups(maxBackups) { try { const files = await fs_extra_1.default.readdir(this.sessionsDir); const autoBackupSessions = []; for (const file of files) { if (file.startsWith('auto-backup-') && file.endsWith('.json')) { const sessionPath = path_1.default.join(this.sessionsDir, file); const session = await fs_extra_1.default.readJSON(sessionPath); autoBackupSessions.push({ name: session.name, createdAt: session.createdAt, path: sessionPath }); } } // Sort by creation date (newest first) and remove excess autoBackupSessions.sort((a, b) => b.createdAt - a.createdAt); if (autoBackupSessions.length > maxBackups) { const toRemove = autoBackupSessions.slice(maxBackups); for (const session of toRemove) { await fs_extra_1.default.remove(session.path); console.log(chalk_1.default.gray(`Cleaned up old auto-backup: ${session.name}`)); } } } catch (error) { console.warn(chalk_1.default.yellow('Warning: Could not cleanup old auto-backups'), error.message); } } } exports.SessionManager = SessionManager; //# sourceMappingURL=session-manager.js.map