claude-playwright
Version:
Seamless integration between Claude Code and Playwright MCP for efficient browser automation and testing
460 lines • 18.3 kB
JavaScript
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
;